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