I am attempting to create a clean cut service layer, whereby the service layer acts upon one or more repositories, and each repositories acts on its own eloquent model.
For example, I may have:
ForumService
|
+-- PostRepo extends PostInterface
| |
| +-- Post (Eloquent)
|
+-- UserRepo extends UserInterface
|
+-- User (Eloquent)
Each service defines it's required dependencies via ioc. So, something like:
// MessageService
// ..
public function __construct(UserInterface $userRepository,
MessageInterface $messageRepository) {
// ..
}
My repositories are resolved via their bindings in their respective service providers, such as:
class UserRepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app>bind(
'App\Models\Repositories\User\UserInterface',
'App\Models\Repositories\User\UserRepository');
}
}
This all works just fine. Each service gets the repositories it requires.
To keep the service layer clear of any specific dependency on eloquent, anything that leaves a repo is a simple, immutable, data object.
Key points in everyday language:
Only the repo's talk to their own models directly
Repo's return simple, immutable, data objects
Services act to tie multiple repo's together and present simplified objects back to the controllers, and ultimately the views.
However I can't come up with a clean pattern to associate eloquent models to each other at the service or repo layer.
Given the Post model has a belongsTo(User::class) relationship, how do I cleanly create that relationship at the Post repository layer.
I have tried:
public function associate($authorId)
{
$post->author()->associate($authorId);
}
But associate expects a user eloquent object, not just an id. I could do:
public function associate($authorId)
{
$post->from()->associate($userRepo->findEloquent($authorId));
}
But I feel like I am surfacing a eloquent model up into a repo that shouldn't be acting on it.
The easy way:
public function assignToAuthor($postId, $authorId)
{
$post = $this->find($postId); // or whatever method you use to find by id
$post->author_id = $authorId;
}
Now, the above implies that you know the foreign key author_id of the relation. In order to abstract it just a bit, use this:
public function assignToAuthor($postId, $authorId)
{
$post = $this->find($postId);
$foreignKey = $post->author()->getForeignKey();
$post->{$foreignKey} = $authorId;
}
Mind, that you still need to save the $post model, but I suppose you already know that.
Depending on your implementation of the simple, immutable, data object that you use, you could also allow passing the objects instead of raw ids. Something between the lines:
public function assignToAuthor($postId, $authorId)
{
if ($postId instanceof YourDataOject) {
$postId = $postId->getId();
}
if ($authorId instanceof YourDataOject) {
$authorId = $authorId->getId();
}
// ...
}
What I've done in the past that has brought some sanity to this situation for me was do things similar to what you are doing in your second associate method and prefix the repository with Eloquent so in the event I use something besides Eloquent, I just create a new implementation of the repository.
So in this case, I'd end up with class EloquentUserRepository implements UserInterface. I usually end up with some public methods which take and return only primitives and possibly some private methods which would be coupled to Eloquent so what I end up doing then is dropping those public methods into a AbstractUserRepository, or a trait if it makes more sense, to keep the code DRY.
It really depends on the situation, I had many thoughts on those actions as well on my repositories.
What I would suggest is to simply not use the "associate" function, you can simply do:
$post->user_id = $userID;
$post->save();
** of course you need to make sure that the user with that id exists.
A) You can do it outside with a special service for "associatingUser"
B) You can do it like you did with using the UserRepositoryInterface,
I see no problem adding the interface as a dependency.
Option A:
class AssociateUserToPost {
private $userRepo;
private $postRepo;
public function __construct(UserRepoInterface $userRepo, PostRepoInterface $postRepo) {
$this->userRepo = $userRepo;
$this->postRepo = $postRepo;
}
public function associate($userId, $postId) {
$user = $this->userRepo->getUser($userId);
if ( ! $user )
throw new UserNotExistException();
$post = $this->postRepo->getPost($postId);
if ( ! $post )
throw new PostNotExistException();
$this->postRepo->AttachUserToPost($postId, $userId);
}
}
option B (quite the same, code just sits in different places)
class PostRepository implements PostRepoInterface {
private $userRepo;
public function __construct(UserRepoInterface $userRepo) {
$this->userRepo = $userRepo;
}
public function associate($userId, $postId) {
$user = $this->userRepo->getUser($userId);
if ( ! $user )
throw new UserNotExistException();
$post = $this->getPost($postId);
if ( ! $post )
throw new PostNotExistException();
$this->AttachUserToPost($postId, $userId);
}
}
Hydration!
I'm assuming that another reason calling findEloquent within the post service seems icky is because you may have already retrieved that data within the controller. Simply put, you can access the same method that Eloquent uses to transform raw query results into fully functioning models.
$userData = array(
// simple, immutable data
);
$userCollection = User::hydrate(array($userData));
$userModel = $userCollection->first();
I think you actually need an additional layer, is what I call a Manager. This will contain all the business logic and will work only with interfaces. Under the hood it will call the services(each knowing to work with a specific resource/model)
Related
I have a scope on my Supplier model that returns results where active = true.
This works great when creating new entries, as I only want the user to see active suppliers.
Current entries may have an inactive supplier; When I edit it, I want to see all active Suppliers, plus the current supplier (if it is inactive)
I have this code in my controller:
$suppliers = Supplier::active()->get();
if (!$suppliers->contains('id', $record->supplier->id))
{
$suppliers->add(Supplier::find($record->supplier->id));
}
Two questions: Is this the correct way to do this? Should this code be in my controller or should I have it somewhere else? (perhaps a scope but I wouldn't know how to code that).
Edit:
Thanks for the help guys. I have applied advice from each of the answers and refactored my code into a new scope:
public function scopeActiveIncluding($query, Model $model = null)
{
$query->where('active', 1);
if ($model && !$model->supplier->active)
{
$query->orWhere('id', $model->supplier->id);
}
}
What you've written will work, but the Collection::contains function can potentially be pretty slow if the collection is large.
Since you have the id, I would probably make the following change:
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
Of course, the downside to this is that you may be making an unnecessary query on the database.
So you have to consider:
is the record's supplier more likely to be active or inactive?
is the size of the collection of active suppliers large enough to justify another (potentially wasted) call to the database?
Make the choice that makes the most sense, based on what you know of your application's data.
As for the second question, if you will only need this specific set of suppliers in this one part of your application, then the controller is a good place for this code.
If, however, you will need this particular set of suppliers in other parts of your application, you should probably move this code elsewhere. In that case, it might make sense to create a function on the the related model (whatever type $record is...) that returns that model's suppliers set. Something like:
public function getSuppliers()
{
$suppliers = Supplier::active()->get();
$supplier = $this->supplier;
if (!$supplier->active) {
$suppliers->add($supplier);
}
return $suppliers;
}
I saw #Vince's answer about 1st question, and I'm agree with him.
About 2nd question:
Write scope in Supplier model like this:
public function scopeActive($query){
$query->where('active', 1); // for boolean type
}
For good practice, you need to write the logic parts in services like "App\Services\SupplierService.php". And there write the function you want:
public function activeSuppliersWithCurrent($record) {
$suppliers = Supplier::active()->get();
$supplier = Supplier::find($record->supplier->id);
if (!$supplier->active) {
$suppliers->add($supplier);
}
}
In your SupplierController's constructor inject the instance of that service and use the function, for example:
use App\Servives\SupplierService;
protected $supplierService = null;
public function __construct(SupplierService $supplierService) {
$this->supplierService = $supplierService;
}
public function getActiveSuppliersWithCurrent(...) {
$result = $this->supplierService->activeSuppliersWithCurrent($record);
}
As you can see, later you will not need to change anything in controller. If you'll need to change for example the query of suppliers selection, you will just have to change something only in service. This way will make your code blocks separated and shorter.
Also the sense for this pattern: you don't need to access the models from controller. All logic related with models will implemented in services.
For other projects you can grab only services or only controllers, and implement another part differently. But in that case if you had all codes in controller, that will prevent you to grab the portions of necessary codes, cuz may you don't remember what doing each blocks...
You could add a where clause to the query to also find that id.
$suppliers = Supplier::active()->orWhere('id', $record->supplier->id)->get();
You could potentially slide this into the active scope by passing the 'id' as an argument.
public function scopeActive($query, $id = null)
{
$query->where('active', true);
if ($id) {
$query->orWhere('id', $id);
}
}
Supplier::active($record->supplier->id)->get();
Or make another scope that does this.
I am trying to update/delete/create in belongsTo relations.
Company has many sports
sports is belonging to Company
Here is two models.
class CompanySports
{
public function company()
{
return $this->belongsTo(Company::class, "company_id","id");
}
class Company
public function sports()
{
return $this->hasMany(CompanySports::class,"company_id","id");
}
}
at controller, when sports is added or modified or remove, what is the best practice to update?
i know that many to many, sync can be used. In this, what is the best solution? Should i compare everytime after loading all from database which is not good practice i believe.
From your code, I would first recommend putting your models in separate files, and ensuring they are singular. If you use the artisan make:model command to generate the stubs, it should do this for you.
// app/CompanySport.php // <-- NOTE singular
class CompanySport // <-- NOTE singular
{
public function company()
{
return $this->belongsTo(Company::class, "company_id","id");
}
}
// app/Company.php
class Company {
public function sports()
{
return $this->hasMany(CompanySport::class,"company_id","id"); // singular
}
}
From there, I find it helpful to build helper methods in the various classes so that the grammar sounds natural and more importantly, belongs to the model. For example:
// app/Company.php
class Company
{
...
public function addSport(CompanySport $sport)
{
$this->sports()->save($sport);
}
public function removeSport(CompanySport $sport)
{
$this->sports()->find($sport->id)->delete();
}
}
These helper functions can then be easily called from anywhere, e.g. controller:
// CompanySportsController.php
public function store(Company $company, CompanySport $sport)
{
$company->addSport($sport);
return redirect('/company/' . $company->id);
}
If you are using these helpers, there is no comparing or sync to be done since you are only using a one to many relationship. Eloquent does everything for you.
Also, I've found this cheatsheet particularly helpful when building out the initial relationships and scaffolding of a new app.
While adding new record of Company Model, you need not to do anything as there is no child for it yet.
While updating an instance of a Company model, again you need not to update anything on its children. As relationship are based on id(primary key) which I believe you don't change while updating.
And now for deleting there are some questions. Do you want to delete the children when the parent is deleting? If so, you can use ON DELETE CASCADE which you can set up in migration like
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
in your spors table.
Well you can make your own function too like answered in here
Well if you don't want to delete the children, you can use softdelete on your Model. set up the relations then like
CompanySports
public function company()
{
return $this->belongsTo(Company::class, "company_id","id")->withTrashed();
}
This way you can get the parent of a children without any error though the parent is deleted.
I'm using parent->child (master->detail) relation in Yii2 Active Record
When I want to create a child, I have to manually fill its parent info like this:
Relation: Client (1) ---> (n) Comments
class ClientController extends \yii\web\Controller
{
public function actionAddComment() {
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->client = $this->id; // Client id
$comment->save();
}
return $this->render('view', ['comment'=>$comment]);
}
}
I've optimized it, creating a Comment method to do that:
class Comment extends ActiveRecord {
public function newComment($client) {
$comment = new Comment;
$comment->client = $client; // Client id
return $comment;
}
}
And I have gone through beforeSave in the Comment model, but still not sure if there is a better way.
Is there anything like:
$comment = new Comment(Yii::$app->request->post());
$client->save($comment); // Here the parent is writing his information to the child
Or one-liner shortcut:
$client->save(new Comment(Yii::$app->request->post());
Without having to create this logic in beforeSave?
Yes, I recommend to use the built in link() and unlink() methods provided by Active Record which you can use in your controller to relate or unrelate 2 models either they share many-to-many or one-to-many relationship.
It even has an optional $extraColumns attribute for additional column values to be saved into a junction table if using it link( $name, $model, $extraColumns = [] )
So your code may look like this :
$comment = new Comment;
if ($comment->load(Yii::$app->request->post())) {
$comment->link('client', $this);
}
check docs for more info.
Now about where to use this code to relate models, it depend on how your app is structured. I'm not sure if doing that through a triggered event would be a good practice, you need to remember that errors may happens and
you may need to evaluate certain scenarios or logic before throwing exceptions. So in my case, I prefer to use that code into my Controllers.
Sometimes you need to build a specific action like you did actionAddComment(), In certain other cases like when your Post request is meant to update the Parent model and also update its related child models at once, the Parent's Update Action ClientController::actionUpdate() may be a good place to do so, maybe something like this will do the job :
$params = Yii::$app->request->post();
$client->load($this->params, '');
if ($client->save() === false && !$client->hasErrors()) {
throw new ServerErrorHttpException('Failed to update the object for unknown reason.');
}
foreach ($params["comments"] as $comment) {
// We may be sure that both models exists before linking them.
// In this case I'm retrieving the child model from db so I don't
// have to validate it while i just need its id from the Post Request
$comment = Comment::findOne($comment['id']);
if (!$comment) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
// according to its documentation, link() method will throw an exception if unable to link the two models.
$comment->link('client', $client);
...
In my project the classes called Object_X represents a single instance of X class (a row in a db table).
I also have to create models, to support these Object_X classes. For example: I think methods like "isEmailAddressTaken($email)" or "isLogged()" (for a customer db table for example) should go in a model (Model_Customer), is that right?
Now, if a class Object_X represents a row in the database and a Model_X represents the db table in general, should the Model_X be a singleton?
I'm kind of confused.
it depends on the task. zend framework has more features besides database access, which might help you.
Specifically, for any kind of validation, you should use Zend_Validate and create validators as you need. for example, for checking if an email is already taken, you should declare Application_Validate_UserEmail_NotExists which extends Zend_Validate_Abstract. then you will be able to use that vaildator class anywhere you need (especially in forms)
<?php
class Application_Validate_UserEmail_NotExists extends Zend_Validate_Abstract
{
const REGISTERED_USER_EMAIL = 'invalid';
const REGISTERED_USER_EMAIL_MESSAGE = 'Email already registered';
protected $_messageTemplates = array(
self::REGISTERED_USER_EMAIL => self::REGISTERED_USER_EMAIL_MESSAGE,
);
public function isValid($value)
{
$modelUsers = new Model_Users();
$user = $modelUsers->fetchRegisteredWithEmail($value);
if (is_object($user)) {
$this->isAlreadyRegisteredError = true;
$this->_error(self::REGISTERED_USER_EMAIL);
return false;
}
return true;
}
}
$validator = new Application_Validate_UserEmail_NotExists();
if ($validator->isValid('email#domain.com')) {
....
}
for checking if the user is logged in, you should use Zend_Auth, which provides variety of functions to handle user authentication and session management.
I'm starting to use Doctrine 2 in a project with a "Group" entity that can inherit from another Group, having the following schema: id | parent_id | name
Because the hierarchy can go deep, I use a linking table, "group_group", using this schema: ancestor_id | descendant_id | depth
The idea is that any group is linked to all of its ancestors and descendants, and the depth field indicates the distance of the relationship, so that I don't have to iterates through parents or children using many SQL requests, a single one can get all the results.
I tried to use Doctrine's ManyToMany relation but I could not get it to be ordered by the depth field, so instead I use the entity's repository to get the related ancestors and descendants.
Because an entity can not access its repository, I would like to know if there is a way for an entity to dispatch events that can be listened by its repository, so that when an entity tries to access its ancestors/descendants, the repository can respond?
Thanks for your help.
An Entity shouldn't have a concrete reference to a Repository but there isn't anything wrong with defining an Interface and have your Repository implement this interface and injecting it into the Entity.
Similar to this solution Doctine 2 Restricting Associations with DQL
interface TreeInterface
{
public function findParent();
public function findChildren();
}
Then your Entities.
class Group
{
$repository;
setRepository(TreeInterface $repository)
{
$this->tree = $repository;
}
public function getParent()
{
return $this->repository->findParent($this);
}
public function getChildren()
{
return $this->repository->findChildren($this);
}
}
class GroupRepository extends EntityRepository implements TreeInterface
{
public function findParent(Group $group)
{
return //stuff
}
public function findChildren(Group $group)
{
return //stuff
}
}
And you use it this way.
$group = $em->find('Group', 1);
$group->setRepository($em->getRepository('Group'));
$children = $group->getChildren();
To avoid setting the repository each time you get a child, I would take a look at the EventManager and postLoad event and see if you can inject a TreeInterface into the Entity on load.