#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
Комментариев нет:
Отправить комментарий