Symfony 3.4 Doctrine: Change referenced table on runtime - php

In my symfony application products are stored in the table products. There is an Entity Product and Repository ProductRepository for it.
Some products automatically get archived and stored in another table, called products_archived. I created an Entity ProductArchived and duplicated the ProductRepository file to the file ProductArchivedRepository.php.
The tables products and products_archived have exactly the same structure and fields.
My goal:
When in the code a product is identified as an archived product, I want to be able to apply a function from the ProductRepository and NOT having to refer to a separate ProductArchivedRepository. I want to avoid having to use duplicated code.
Example:
ProductRepository.php:
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
ProductArchivedRepository.php:
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
ProductService.php:
public function getProductDataById($productId)
{
$repoProduct = $this->productRepository;
$repoProductArchived = $this->container->productArchivedRepository;
if ($repoProduct->findOneBy(['id' => $productId]) instanceof Product) {
$repoP = $repoProduct;
} else if ($repoProductArchived->findOneBy(['id' => $productId]) instanceof ProductArchived) {
$repoP = $repoProductArchived;
} else {
throw new NotFoundHttpException(
'Product neither found in table product nor in table product_archived.'
);
}
$productData = $repoP->getProductDataById($productId);
return $productData;
}
How do I achieve that ProductArchivedRepository.php becomes redundant?

You can use an abstract class that regroup the duplicated method(s)
abstract class AbstractProductRepository extends EntityRepository
{
public function getProductDataById($productId)
{
$qb = $this->createQueryBuilder('p');
// ...
return $qb->getQuery()->getArrayResult();
}
}
And now, both of your repository can extend the abstract class instead of EntityRepository :
class ProductRepository extends AbstractProductRepository
{
// ...
}
class ProductArchivedRepository extends AbstractProductRepository
{
// ...
}

Related

Symfony: Dynamic route for multiple entities

Currently working with Symfony 5.2
I am trying to create a dynamic route for multiple enties which have mostly same properties (some still have other fields too, but all have the same default properties).
Example (minified)
Entity News:
- id, title, author
Entity Event:
- id, title, author
I am now trying to create a dynamic route to fetch News or Events from my the repository.
class ContentController extends AbstractController {
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type) { //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository(/* entity class */)->findAll();
}
}
I already came up with some ideas, but not sure if they are good practise.
1) use full namespace for the entity (how do I check if the class exists? error handling?)
$results = $this->getDoctrine()->getRepository('App\\Entity\\News')->findAll();
$results = $this->getDoctrine()->getRepository('App\\Entity\\' . ucfirst($type))->findAll();
2) provide a route for each entity and call a generic function (not exactly what I want because I have like 10+ entities for this)
class ContentController extends AbstractController {
/**
* #Route("/news", name="get_news")
*/
public function get_news() {
$results = getContent(News::class);
}
/**
* #Route("/event", name="get_event")
*/
public function get_event() {
$results = getContent(Event::class);
}
public function getContent($class) {
return $this->getDoctrine()->getRepository($class)->findAll();
}
}
Mabye some of you have better ideas/improvements and can help me out a bit.
You can define available for fetching entities manually.
class ContentController extends AbstractController
{
/**
* #Route("/{type}", name="get_content")
*/
public function get(string $type)
{ //type is the name of the entity e.g. 'news' or 'event'
//fetch from repo
$results = $this->getDoctrine()->getRepository($this->getEntityClassFromType($type))->findAll();
}
private function getEntityClassFromType(string $type): string
{
//define your entities manually
foreach ([News::class, Event::class] as $class) {
$parts = explode('\\', $class);
$entity = array_pop($parts);
if ($type === lcfirst($entity)) {
return $class;
}
}
throw new NotFoundHttpException();
}
}
Or check your entities dynamically using $entityManager->getMetadataFactory()->hasMetadataFor($className);

Wrapping eloquent model to another class causes exceeded nesting

I'm using Laravel 5.5. I wrote a wrapper that takes an Eloquent model and wraps it to an Entity class and each model has own wrapper. Assume, the User has many products and a Product belongs to one user. When wrapping, I need to get products of a user and pass them to product wrapper to wrap them into the product entities. In the product wrapper, I need to get user owner of this product to wrap it to the user entity. So again, In the user wrapper, I need user products!, and this creates an infinite loop.
EntityWrapper:
abstract class EntityWrapper
{
protected $collection;
protected $entityClass;
public $entity;
public function __construct($collection)
{
$this->collection = $collection;
$this->entity = $this->buildEntity();
}
protected function buildEntity()
{
$tempEntity = new $this->entityClass;
$Entities = collect([]);
foreach ($this->collection as $model) {
$Entities->push($this->makeEntity($tempEntity, $model));
}
return $Entities;
}
abstract protected function makeEntity($entity, $model);
}
UserEntityWrapper:
class UserEntityWrapper extends EntityWrapper
{
protected $entityClass = UserEntity::class;
protected function makeEntity($userEntity, $model)
{
$userEntity->setId($model->user_id);
$userEntity->setName($model->name);
// set other properties of user entity...
//--------------- relations -----------------
$userEntity->setProducts((new ProductEntityWrapper($model->products))->entity);
return $userEntity;
}
}
ProductEntityWrapper:
class ProductEntityWrapper extends EntityWrapper
{
protected $entityClass = ProductEntity::class;
protected function makeEntity($productEntity, $model)
{
$productEntity->setId($model->product_id);
$productEntity->setName($model->name);
// set other properties of product entity...
//--------------- relations -----------------
$productEntity->setUser((new UserEntityWrapper($model->user))->entity);
return $productEntity;
}
}
UserEntity:
class UserEntity
{
private $id;
private $name;
private $products;
//... other properties
public function setProducts($products)
{
$this->products = $products;
}
// other getters and setters...
}
When I wnat to get user entities by calling (new UserEntityWrapper(User::all()))->entity, It causes infinite loop. So, how can I prevent the nesting call to relationship between models? Thanks to any suggestion.
Finally I found the solution. As in each wrapper class, I used the dynamic property to get the relationship collection, in addition to imposing extra queries, this causes lazy loading. So, before passing the model collection into each wrapper, the necessary relationship model is retrieved and each wrapper firstly checks the existence of relationship using method getRelations() (that returns an array of available relations). If intended relationship is available, the collection of relationship models is passed into the proper wrapper class.
UserEntityWrapper:
class UserEntityWrapper extends EntityWrapper
{
protected $entityClass = UserEntity::class;
protected function makeEntity($userEntity, $model)
{
$userEntity->setId($model->user_id);
$userEntity->setName($model->name);
// set other properties of user entity...
//--------------- relations -----------------
$relations = $model->getRelations();
$products = $relations['products'] ?? null;
if ($products) {
$userEntity->setProducts((new ProductEntityWrapper($products))->entity);
}
return $userEntity;
}
}
And, a similar functionality is used for the other wrappers.

Is there a way to decouple Laravel Eloquent models from Service or Controller level?

There is a simple Laravel Eloquent Model below:
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
}
and it's normal to use repository pattern to work with model, like:
use Product;
class ProductRepository implement ProductRepositoryInterface
{
public function __construct(Product $model)
{
$this->model = $model;
}
public function findById($id)
{
return $this->model->find($id);
}
...
}
The controller use the repository to get Prodcut data:
class ProductController extends Controller
{
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->productRepository = $productRepository;
}
public function getSomeInfoOfProduct($id)
{
$product = $this->productRepository->findById($id);
return [
'name' => $product->name,
'alias' => $product->alias,
'amount' => $product->amount,
];
}
}
In the method getSomeInfoOfProduct, when I am deciding what kind of information should I return, I don't know there are how many properties the $product object has until I look at the schema of table products or migration files.
It's look like that the controller is tightly coupled with Eloquent models and the database. If one day, I store the raw data of products in Redis or other places, I still need to create a Eloquent model object, and fill in the object with the data from Redis.
So I am considering to create a pure data object to replace the Eloquent Model object, like below:
class ProductDataObject
{
private $name;
private $alias;
private $amount;
private $anyOtherElse;
public function getName() {
return $this->name;
}
....
}
and let the repository return this object:
use Product;
use ProductDataObject;
class ProductRepository implement ProductRepositoryInterface
{
public function __construct(Product $model)
{
$this->model = $model;
}
public function findById($id)
{
$result = $this->model->find($id);
// use some way to fill properties of the object
return new ProductDataObject(...);
}
...
}
In the controller or service level, I can just look at ProductDataObject to get all information I need. And it also looks like easier to change data storage without affecting the controllers and services.
Does this way make sense?
I think what you're looking for is the Factory Pattern. You're kind of on the right track already. Basically you have a middle-man class that your Controller or Repository basically asks to supply them with the appropriate Model. Through either parsing conditions or a config file using .envs, it figures out which one to serve up, so long as anything it returns all implements the same Interface.

Laravel retrieving eloquent nested eager loading

In my application i have 4 models that relate to each other.
Forms->categories->fields->triggers
What I am trying to do is get the Triggers that refer to the current Form.
Upon researching i found nested eager loading, which would require my code to look like this
Form::with('categories.fields.triggers')->get();
Looking through the response of this i can clearly see the relations all the way down to my desired triggers.
Now the part I'm struggling with is only getting the triggers, without looping through each model.
The code i know works:
$form = Form::findOrFail($id);
$categories = $form->categories;
foreach ($categories as $category) {
$fields = $category->fields;
foreach ($fields as $field) {
$triggers[] = $field->triggers;
}
}
I know this works, but can it be simplified? Is it possible to write:
$form = Form::with('categories.fields.triggers')->get()
$triggers = $form->categories->fields->triggers;
To get the triggers related? Doing this as of right now results in:
Undefined property: Illuminate\Database\Eloquent\Collection::$categories
Since it is trying to run the $form->categories on a collection.
How would i go about doing this? Do i need to use the HasManyThrough relation on my models?
My models
class Form extends Model
{
public function categories()
{
return $this->hasMany('App\Category');
}
}
class Category extends Model
{
public function form()
{
return $this->belongsTo('App\Form');
}
public function fields()
{
return $this->hasMany('App\Field');
}
}
class Field extends Model
{
public function category()
{
return $this->belongsTo('App\Category');
}
public function triggers()
{
return $this->belongsToMany('App\Trigger');
}
}
class Trigger extends Model
{
public function fields()
{
return $this->belongsToMany('App\Field');
}
}
The triggers run through a pivot table, but should be reachable with the same method?
I created a HasManyThrough relationship with unlimited levels and support for BelongsToMany:
Repository on GitHub
After the installation, you can use it like this:
class Form extends Model {
use \Staudenmeir\EloquentHasManyDeep\HasRelationships;
public function triggers() {
return $this->hasManyDeep(Trigger::class, [Category::class, Field::class, 'field_trigger']);
}
}
Form::with('triggers')->get();
Form::findOrFail($id)->triggers;

Yii2 // Model as Model attribute

actually im working on a project, where i want to have all DB-tables as Models. But now im stucking at one Point.
Lets say i have a "Master"-Table where many different relations are defined like the following (easy example):
Human has one heart; Human has one brain... and so on...
Is it possible, to fill up the Master-Model with other Models?
In PHP it would looks like that:
$human = new Human();
$human->heart = new Heart();
$human->brain = new Brain();
Finally i want to say:
$human-save(TRUE);
to VALIDATE all relational models AND save all relational data and the human object in DB.
Is that possible? I cant find something like that on the whole internet O_o.
Thank you very much!
You can override ActiveModel Save method, according to docs:
public function save($runValidation = true, $attributeNames = null)
{
if ($this->getIsNewRecord()) {
$save = $this->insert($runValidation, $attributeNames);
} else {
$save = $this->update($runValidation, $attributeNames) !== false;
}
/* Condition Work if heart and brain is also ActiveModel then
you can trigger save method on these models as well
or you can add your custom logic as well.
*/
if($this->heart && $this->brain) {
return $this->heart->save() && $this->brain->save();
}
return $save;
}
I suggest you following approach:
Let's say you have same relation names as property names for nested objects (some rule needed to call $model->link() method)
Declare common class for Models with nested Models (for example ActiveRecordWithNestedModels)
Override in common class methods save and validate to perform cascade for these operations (using reflection)
Let your models will inherit this common class
Or, as an alternative for overriding validate method, you can build some suitable implementation for rules method in common class.
This common class can looks as follows (this is a simple draft, not tested, just to show the conception):
<?php
namespace app\models;
use yii\db\ActiveRecord;
class ActiveRecordWithNestedModels extends ActiveRecord
{
public function save($runValidation = true, $attributeNames = null)
{
$saveResult = parent::save($runValidation, $attributeNames);
$class = new \ReflectionClass($this);
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$propertyValue = $property->getValue($this);
if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
/* #var ActiveRecord $nestedModel */
$nestedModel = $propertyValue;
$nestedModel->save($runValidation);
$relation = $property->name;
$this->link($relation, $nestedModel);
}
}
return $saveResult;
}
public function validate($attributeNames = null, $clearErrors = true)
{
$class = new \ReflectionClass($this);
foreach ($class->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$propertyValue = $property->getValue($this);
if (!empty($propertyValue) && is_subclass_of($propertyValue, ActiveRecord::className())) {
/* #var ActiveRecord $nestedModel */
$nestedModel = $propertyValue;
if (!$nestedModel->validate(null, $clearErrors)) {
array_push($this->errors, [
$property->name => $nestedModel->errors
]);
}
}
}
parent::validate($attributeNames, $clearErrors);
if ($this->hasErrors()) return false;
return true;
}
}
Then your models can looks like this:
class Heart extends ActiveRecordWithNestedModels
{
}
class Human extends ActiveRecordWithNestedModels
{
/* #var Heart $heart */
public $heart = null;
/**
* The relation name will be 'heart', same as property `heart'
*
* #return \yii\db\ActiveQuery
*/
public function getHeart()
{
return $this->hasOne(Heart::className(), ['id', 'heart_id']);
}
}
And (in theory) you can do:
$human = new Human();
$human->heart = new Heart();
$human->save();
P.S. here can be many complex details in further implementation, as for example
using transactions to rollback save if some child object fails save
overriding delete
serving one-to-many and many-to-many relations
skip cascade if property has no corresponding relation
serving $attributeNames in cascade operations
etc

Categories