In a monolithic architecture, there are 3 main domains such as admin panel, web application and api. Naturally, they all use the same model, but they have some differences. In the admin panel, we list all statuses from the data lists, but in web and api, we only list the published active ones. In this case, what kind of logic is needed in structures such as service, repository, DTO?
If we create a separate service, repository, DTO for each, there will be repetitions.
How do you approach the subject?
#Which patterns and structures do you prefer in your applications?
24 messages · Page 1 of 1 (latest)
A good approach to avoid repetition while maintaining flexibility is to use a base repository, service, and DTO structure with specific filtering logic applied based on the context (admin panel, web, or API).
Need to create a BaseRepository that handles common queries, and extend it for specific use cases.
abstract class BaseRepository {
protected $model;
public function __construct(Model $model) {
$this->model = $model;
}
public function all() {
return $this->model->all();
}
public function find($id) {
return $this->model->find($id);
}
public function query() {
return $this->model->newQuery();
}
}
Then, create a specific repository, like PostRepository, that extends BaseRepository
class PostRepository extends BaseRepository {
public function __construct(Post $post) {
parent::__construct($post);
}
public function getPublished() {
return $this->query()->where('status', 'published')->get();
}
public function getAllStatuses() {
return $this->all();
}
}
Instead of creating separate services, you are able to create a single service with context-aware methods.
class PostService {
protected $postRepository;
public function __construct(PostRepository $postRepository) {
$this->postRepository = $postRepository;
}
public function getPostsForAdmin() {
return $this->postRepository->getAllStatuses();
}
public function getPostsForWeb() {
return $this->postRepository->getPublished();
}
public function getPostsForApi() {
return $this->postRepository->getPublished();
}
}
and then should create a single PostDTO and apply transformations based on context.
class PostDTO {
public static function fromModel(Post $post, $context = 'web') {
return [
'id' => $post->id,
'title' => $post->title,
'content' => $post->content,
'status' => $context === 'admin' ? $post->status : null, // Hide status in web/api
'published_at' => $post->published_at,
];
}
}
and this is controller
class AdminPostController {
protected $postService;
public function __construct(PostService $postService) {
$this->postService = $postService;
}
public function index() {
return response()->json($this->postService->getPostsForAdmin());
}
}
class WebPostController {
protected $postService;
public function __construct(PostService $postService) {
$this->postService = $postService;
}
public function index() {
return response()->json($this->postService->getPostsForWeb());
}
}
you can structure like above
Thanks for the answer. The current structure is like this. I wonder how it could have been different. Of course, there is a simple approach in the examples, but in reality, we do a lot of logic operations in the service layer. The important thing is to prevent repetition through them. This is a question asked in different ways.
Have you ever used Context-Based strategies?
for example...
class PostService {
protected $postRepository;
public function __construct(PostRepository $postRepository) {
$this->postRepository = $postRepository;
}
public function getPosts($context = 'web') {
$query = $this->postRepository->query();
if ($context !== 'admin') {
$query->where('status', 'published');
}
return $query->get()->map(fn ($post) => PostDTO::fromModel($post, $context));
}
}
@sacred leaf Hi, do you have any project in the open source repository so I can see these practices?
I'm also interested in this and have a similar implementation (I can share if necessary) and want to choose useful practices for myself
no project related to your question. it is my thought.
you can use it in controller.
class AdminPostController {
public function index(PostService $postService) {
return response()->json($postService->getPosts('admin'));
}
}
class WebPostController {
public function index(PostService $postService) {
return response()->json($postService->getPosts('web'));
}
}
or
another my thought...
If the filtering is purely based on user roles, move it to middleware or policies instead of repeating it in services... like that
class FilterPublishedPosts {
public function handle($request, Closure $next) {
if (!auth()->user()?->isAdmin()) {
Post::addGlobalScope('published', function ($query) {
$query->where('status', 'published');
});
}
return $next($request);
}
}