Страницы

Поиск по вопросам

пятница, 27 декабря 2019 г.

Имплементация Repository паттерна в Laravel

#laravel #шаблоны_проектирования #php #repository


Всем привет!
С недавних пор изучаю Laravel 4 и его возможности. Стала задача имплементировать
паттерн Repository, чтобы вынести логику работы с бд туда. И вот тут столкнулся с рядом
неудобств или непониманием, как правильно все организовать. Общий вопрос у меня звучит
примерно так: возможна ли реализация и применение этого паттерна в Laravel без лишней
головной боли и стоит ли оно того?
Вопрос хотел бы разбить на несколько частей, которые вызвали у меня смятения.
1) В Laravel есть возможность на стадии определения роута биндить модель в качестве
параметра контроллера (например):
// routes.php
Route::bind('article', function($slug)
{
    return Article::where('slug', $slug)->first();
});

Route::get('articles/{article}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    public function getArticle(Article $article)
    {
        return View::make('article.show', compact('article'));
    }
}

Если я хочу использовать паттерн Repository, то я не могу использовать такой подход,
т.к. в этом случае контроллер явно будет знать о существовании модели Article? Правильно
ли будет переписать этот пример с использованием Repository таким образом:
// routes.php
Route::get('articles/{slug}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    private $article;

    public function __construct(ArticleRepository $article) {
        $this->article = $article;
    }

    public function getArticle($slug)
    {
        $article = $this->article->findBySlug($slug);

        return View::make('article.show', compact('article'));
    }
}

2) Допустим, мой вариант из предыдущего пункта с применением Repository оказался
удачным. Теперь я хочу, чтобы при просмотре статьи у нее увеличивался счетчик просмотров,
при этом я хочу вынести эту обработку в Event. То есть код будет следующим:
// routes.php
Route::get('articles/{slug}', 'ArticlesController@getArticle');

// controllers/ArticlesController.php
class ArticlesController extends BaseController {

    private $article;

    public function __construct(ArticleRepository $article) {
        $this->article = $article;
    }

    public function getArticle($slug)
    {
        $article = $this->article->findBySlug($slug);
        Events::fire('article.shown');

        return View::make('articles.single', compact('article'));
    }
}

// некий subscriber, подписанный на события
class ArticleSubscriber {

    public function onShown()
    {
        // почему нету реализации, описано ниже
    }

    public function subscribe($events)
    {
        $events->listen('article.shown', 'ArticleSubscriber@onShown');
    }

}

На данном этапе я снова был озадачен тем, как правильно реализовать обработку события.
Передать модель статьи $article в событие я не могу, т.к. это опять нарушает принципы
ООП и мой subscriber будет знать о существовании модели article. То есть сделать так
я не могу:
// controllers/ArticlesController.php
...
\Events::fire('article.shown', $article);
...

// некий subscriber, подписанный на события
...
public function onShown(Article $article)
{
    $article->increment('views');
}
...

С другой стороны, внедрять в subscriber репозиторий ArticleRepository я тоже не вижу
смысла, потому что мне снова придется сперва найти статью, а потом обновить ее счетчик,
в итоге получится лишний запрос к бд:
// controllers/ArticlesController.php
...
Events::fire('article.shown', $slug);
...

// некий subscriber, подписанный на события
...
private $article;

public function __construct(ArticleRepository $articleRepository) 
{
    $this->article = $articleRepository;
}

public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    $article->increment('views');
}
...

Более того, после того, как Event отработал (т.е. увеличил счетчик просмотров), необходимо,
чтобы контроллер знал об обновленной модели, т.к. в представлении нужно вывести обновленный
счетчик просмотров. Получается, что каким-то образом мне еще и необходимо вернуть новую
модель из Event, но не хотелось бы, чтобы Event становился обычным методом для обработки
какого-то действия (для этого ведь есть репозиторий) и возвращал какое-то значение.
Вдобавок ко всему, вы можете заметить, что моя последняя реализация onShow() снова
противоречит правилам паттерна Repository, но я не понимаю, как вынести эту логику
в репозиторий:
public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    // НЕВЕРНО! т.к. Event не должен знать о том, что умеет модель в реализации Eloquent
    // $article->increment('views');
}

Можно ли найденную модель передать обратно в репозиторий и уже там увеличить ей счетчик
(имеется ввиду, не противоречит ли такой подход паттерну?)? Примерно так:
public function onShown($slug)
{
    $article = $this->articleRepository->findBySlug($slug);
    $this->articleRepository->updateViews($article);
}

// ArticleRepository.php
...
public function updateViews(Article $article) {
    $article->increment('views');
}
...

В качестве итога попробую сформулировать все компактнее:


При использовании паттерна
    Repository мне придется отказаться
    от передачи модели в контроллер и подобных удобностей?


Возможно ли при использовании
    репозитория держать в нем состояние
    модели и передавать его между
    сущностями (например, из фильтра в
    контроллер, из контроллера в Event и
    обратно) во избежание непотребных
    повторных обращений к бд, и
    правильный ли это будет подход
    (сохранение состояния модели)?


Такие дела, такие вот у меня вопросы. Хотелось бы услышать ответы, мысли, комментарии.
Быть может, я не так пытаюсь применить паттерн? Сейчас он вызывает больше головной
боли, нежели решает проблему дата маппинга.    


Ответы

Ответ 1



Приветствую! Раз Вам нужно инкрементировать счётчик всегда, когда открывается статья, то не лучше ли будет вызывать increment() непосредственно из метода репозитория findBySlug($slug) и делать вызов не от модели $article, а сделать это методом самого репозитория? Что касается подхода с использованием событийной модели, то тут можно предложить использовать принцип Command-Query separation с использованием того же Commander от Джеффри (и/или Querier, что в данном случае больше подойдёт по концепции). При этом в контроллере у Вас будет вызов команды (тут и далее пишу так, если Вы будете пользовать Commander): // controllers/ArticlesController.php class ArticlesController extends BaseController { public function getArticle($slug) { $article = $this->execute(ArticleShowCommand::class, compact('slug')); return View::make('article.show', compact('article')); } } В команде у Вас будет получение из репозитория и вызов сопутствующих событий: // Site/Commands/Article/ArticleShowCommandHandler.php use Laracasts\Commander\CommandHandler; use Laracasts\Commander\Events\DispatchableTrait; class ArticleShowCommandHandler implements CommandHandler { use DispatchableTrait; private $article; public function __construct(ArticleRepository $articleRepository) { $this->article = $articleRepository; } public function handle($command) { $article = $this->articleRepository->findBySlug($command->slug); $this->dispatchEventsFor($article); return $article ; } } А в репозитории, при выдаче данных, заводите нужное событие: // Site/Repositories/ArticleRepository.php public static function findBySlug($slug) { $article = Article::where('article_alias', $slug); $article->raise(new ArticleWarViewed($article)); return $job; } Ну и уже создание листнеров нужных и реализация там нужной логики по инкрементации просмотров и прочего - смотрите README.md в репозитории...

Ответ 2



http://fideloper.com/hexagonal-architecture

Комментариев нет:

Отправить комментарий