Relations inside DTO and php7.4 : how to hydrate? - php

I wonder what is the good practices :
Let's say I have 2 entities, ManyToOne. Both are ApiResources, and both have an Output DTO. So Both have a transformer.
<?php
/**
* #ORM\Entity
* #ApiResource(
* output="Dto\Foo"
* )
*/
class Foo
{
private int $id;
/**
* #ORM\ManyToOne(targetEntity="Bar")
*/
private Bar $bar;
}
Problem is, when I transform the entity Foo into a DTO Foo, I want to hydrate it with a Bar DTO, not a Bar entity. But since I hydrate it with from an entity, I have a Bar entity. Later in the process, the Bar entity is replaced by a Bar DTO, ApiPlateform is working, but my mental problem is : the bar property type is modified over time (Moreover it can't be Typehinted). Seems dirty to me, isn't it ?
Illustration:
the Transofmer
<?php
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
class FooEntityToFooDToTransormer implements DataTransformerInterface
{
public function transform($object, string $to, array $context = [])
{
return new FooDto($object);
// maybe there is a better way to hydrate FooDto, by getting directly a BarDto here ?
}
}
The DTO :
<?php
namespace Dto;
class Foo
{
public int $id;
// problem is I cant typehint here
public $bar;
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = $fooEntity->getBar(); // <-- return a Bar entity, transformed later by ApiPlatform into a Bar DTO.
}
}
It there a way or a good practice to proper hydrate a DTO from an entity, especially about relations ?
Edit :
I actually prefer not Typehint $bar as its normalization (so its transformation) should be handled by ApiPlateform. But then, circular references are not handeled (memory limit) and I don't really know why (probably confusion between object and DTO).
I don't think my FooTransformer should know how to transform Bar, because according to the context I could need one transformer or another, or an IRI... Test all of them with "supportTransformation" and so, for every relation ? And what about circular ?
It's a little mess, my solution for now is to choose to return FooDto (without transform barDto) or to return an IRI, according to the context (which I am absolutly not sure of what I'm doing with it due to the lack of documentation about $context).
Same for BarTransformer.
So every transformer need to choose to actually transform the object without handeling transformation of relations, or return the correspondant IRI. That is the less dirty I found.

I guess you have two possible solutions here, to extend your DTO's constructor signature by one more argument and adjust your transformer or to do the transformation right inside your DTO's constructor:
<?php
namespace Dto;
class FooDto
{
public BarDto $bar;
// first variant
public function __construct(FooEntity $fooEntity, BarDto $barDto)
{
$this->id = $fooEntity->getId();
$this->bar = $barDto;
}
// second
public function __construct(FooEntity $fooEntity)
{
$this->id = $fooEntity->getId();
$this->bar = new BarDto($fooEntity->getBar());
}
}

Related

How I can use Polymorphism to return multiple results and use multi behaviours in the same time

I'm using Laravel 9 and I have a request can contains :
Parameter called SEASON the value can be an array or null
so SEASON parameter can be an array and can be also null
Parameter called EXPIRY can be an array and can be also null
I have two classes one for the SEASON feature and the other class for EXPIRY both they extends from Repository. and both have a method called execute that return an array
abstract class Repository
{
abstract public function execute(): array;
}
class Expiry extends Repository
{
public function execute()
{
return ['The Request contain Expiry Parameter, and seasonal behaviours is done'];
}
}
class Season extends Repository
{
public function execute()
{
return ['The Request contain Season Parameter, and expiry behaviours is done'];
}
}
I would like to call execute method of Season class if my request contains SEASON, or call the execute method of expiry if my request contains Expiry. OR Call both of them and merge the execute return of execute in one array so I can have as result.
['The Request contain Expiry Parameter, and seasonal behaviours is done', 'The Request contain Expiry Parameter, and expiry behaviours is done']
That's what I tried inside my controller :
public function bootstrap($data)
{
$parseTopics = Helper::parseTopicsRequest();
$basicProgram = new BasicProgramRepository();
$seasonalProgram = new SeasonalProgramRepository($parseTopics['SEASONAL']);
$object = count($parseTopics['SEASONAL']) ? $seasonalProgram : $basicProgram;
// Polymorphism
return $object->execute();
}
Question 1 :
I'm not sure if I should use this way or something like to fix my need:
$employe = new Program(new BasicProgramRepository());
Expected Result :
The expected result depends on if I have season parameter and expiry. What I want to achieve is to use different behaviours ( execute method )
if you want to achieve Polymorphism method, it will be better creating repository or something only for managing that logic.
here is sample.
class SampleRepository
{
/**
* repository instance value
*
* #var string[] | null
*/
private $sampleArray; // maybe here is SEASON or EXPIRY or null
/**
* constructor
*
* #param string[] | null $sampleArray
*/
public function __construct($sampleArray)
{
$this->sampleArray = $sampleArray;
}
/**
* execute like class interface role
*
* #return array
*/
public function execute()
{
return (!$this->sampleArray) ? [] : $this->getResult();
}
/**
* get result
*
* #return array
*/
private function getResult()
{
// maybe pattern will be better to manage another class or trait.
$pattern = [
"SEASON" => new Season(),
"EXPIRY" => new Expiry()
];
return collect($this->sampleArray)->map(function($itemKey){
$requestClass = data_get($pattern,$itemKey);
if (!$requestClass){ // here is space you don't expect class or canIt find correct class
return ["something wrong"];
}
return $requestClass->execute();
})->flatten();
}
}
and you can call like this.
$sampleRepository = new SampleRepository($sampleValue); // expect string[] or null like ["SEASON"],["SEASON","EXPIRY"],null
$result = $sampleRepository->execute(); // [string] or [string,string] or []
this approach is only what your parameter is secified value.
if your return result is almost same both of Season class and Expiry class, it will be better to manage on trait. (that is $pattern on sample code)
try some.
I read comments,so following..
For example, it prefers to be only getting result of getResult().
so, some pattern and so many logics shouldn't be written on getResult();
If you use trait, this is sample.
first, you need to create managing behaviors class.
Behavior.php
<?php
namespace App\Repositories;
class Behavior
{
use Behavior\BehaviorTrait;
// if you need to add another pattern, you can add trait here.
}
and then, you need to create Behavior directory at same level place.
you move that directory, you create trait file like this.
<?php
namespace App\Repositories\Behavior;
trait BehaviorTrait
{
public static function findAccessibleClass(string $itemKey)
{
return data_get([
"SEASON" => new Season(),
"EXPIRY" => new Expiry()
],$itemKey);
}
}
findAccessibleClass() method has responsible of finding correct class.
then, you call this method like this.
private function getResult()
{
return collect($this->sampleArray)->map(function($itemKey){
$requestClass = Behavior::findAccessibleClass($itemKey); // fix here.
if (!$requestClass){ // here is space you don't expect class or canIt find correct class
return ["something wrong"];
}
return $requestClass->execute();
})->flatten();
}
if your code is so much in getResult(), you will be better to separate code for responsible.
To create Behavior trait, getResult don't need to have responsible of behavior logic. it will be easy testing or fixable in short.
hope well.

PHP/MVC : in this case, how to use DTO?

I'm new to OOP and MVC with PHP, and I'm currently learning by making my own custom framework from scratch, for testing purposes. I have set up my controllers, models and views and everything works fine.
My app has the following architecture :
It’s a small blog that follows the rules of the MVC pattern. To summarize, it works like this :
The called Controller will fetch the data using the right models
Models return objects of the class \Classes\{MyObject}
Controller call the right template to render the view, and passes it the data and objects to display
The problem
In some views, I need to display related data. For example, in the article view, I need to display the author's first name. In the database, an article contains only the author’s ID, not his first name : this is the same thing in my class \Classes\Article.
What I've tried
To display the author’s first name in my view, I've updated the model Find method to use a LEFT JOIN in the SQL query. Then, I've updated my \Classes\Article class to have a user_firstname property :
class Article
{
private $pk_id;
private $title;
private $excerpt;
private $content;
private $created_at;
private $fk_user_id;
private $updated_at;
private $user_firstname; // <-- I've added this property to retrieve author's firstname
// (...)
}
What I did works well, but my teacher tells me it’s not the right way to do it because the author’s firstname is not part of the definition of an article.
In this case, my teacher tells me to use a DTO (Data Transfert Object) between Article and User classes.
Questions
What is the right way to set up a DTO in this case?
Do I need to create a new ArticleUserDTO class in a new namespace ?
How to use it ?
I think I understood the problem : the Article class should only contain what defines an article. But I can’t understand the logic of setting up a DTO. I’ve done some research on it, I understand the usefulness of the DTO but I can’t set up into my app.
Setting up a DTO in my app was easy ! As mentioned by #daremachine, the diagram below helped me to understand what a DTO is for.
Diagram source : martinfowler.com
We can see DTOs as an object assembler, in which we place all the elements we need on the view side.
For example, in the post view of an article, I needed to display other items, such as the author and posted comments. So I have created a Post class that groups all these items.
Setting up a DTO
In my \Classes\ namespace, I've created a new Post class. First, we define the properties we will need. Then we add the getters and setters for each of them. Finally, we set up the constructor, which will call each of the classes we need in the view.
namespace Classes;
use DateTime;
class Post
{
private int $pk_id;
private string $title;
private string $excerpt;
private string $content;
private DateTime $created_at;
private DateTime $updated_at;
private int $author_id;
private string $author_firstname;
private array $comments;
public function __construct(Article $article, User $author, array $comments)
{
$this->setPkId($article->getId());
$this->setTitle($article->getTitle());
$this->setExcerpt($article->getExcerpt());
$this->setContent($article->getContent());
$this->setCreatedAt($article->getCreatedAt());
$this->setUpdatedAt($article->getUpdatedAt());
$this->setAuthorId($article->getAuthorId());
$this->setAuthorFirstname($author->getFirstname());
$this->setComments($comments);
}
/**
* #param int $pk_id
*/
public function setPkId(int $pk_id): void
{
$this->pk_id = $pk_id;
}
/**
* #return int
*/
public function getPkId(): int
{
return $this->pk_id;
}
// (etc)
}
We now need to update the ArticleController, which should no longer pass the Article, Comment and User objects, but only the new Post object.
namespace Controllers;
class ArticleController extends Controller
{
// (...)
/**
* Get an article and display it
*
* #return void
*/
public function show(): void
{
// (...)
// Find Article :
$article = $this->articleModel->find($article_id);
if (!$article) {
Http::error404();
}
// Find Comments :
$commentaires = $this->commentModel->findAllByArticle($article_id);
// Find User (author)
$user = $this->userModel->find($article->getAuthorId());
// Data Transfert Object instance :
$post = new Post($article, $user, $commentaires);
$pageTitle = $post->getTitle();
// Pass DTO to view :
Renderer::render('articles/show', compact('pageTitle', 'post'));
}
}
We just need to update our view to use the new Post object and it's done ! Thanks to #daremachine for his help :)

How should I check for dupplicate entity in an object collection?

I've been reading this post : Doctrine - how to check if a collection contains an entity
But I actually don't like the solution, as, doctrine already provide the contains() method, which have the advantage to keep logic directly into the object, and then to not load EXTRA_LAZY collections entirely.
So here a Cart Entity own a CartProduct collection as is :
/**
* ...
* #ORM\Entity(repositoryClass="App\Repository\CartRepository")
*/
abstract class Cart implements InheritanceInterface{
...
/**
* #ORM\OneToMany(targetEntity="CartProduct", mappedBy="cart", fetch="EXTRA_LAZY", cascade={"persist"})
*/
private Collection $cartProducts;
...
public function __construct()
{
$this->cartProducts = new ArrayCollection();
}
...
}
(CartProduct have to be an Entity look at this simplify EA model. That's a standard way to proceed for related entity holding extra fields)
Now I want to add a new ProductCart Entity to my Cart class.
So I'm adding this method (generated by Symfony make:entity) :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->contains($cartProduct)) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
And then I test this code :
public function testAddCartProduct()
{
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
}
But when I run this test, it fail :
Failed asserting that actual size 2 matches expected size 1.
So I check, and the Cart.cartProducts Collection have two product which are exactly the same objects.
As it's an ArrayCollection, I suppose that it just use this method :
namespace Doctrine\Common\Collections;
class ArrayCollection implements Collection, Selectable {
...
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So well, of course in this case it is just return false, And the objects are considered to be different.
So now, I wish I could use PersistentCollection instead of ArrayCollection when implementing the Collection object , because the PersistentCollection.contains() method looks better.
abstract class Cart implements InheritanceInterface{
...
public function __construct()
{
-- $this->cartProducts = new ArrayCollection();
++ $this->cartProducts = new PersistentCollection(...);
}
}
But this require an EntityManager as a parameter, so, seams a little bit overkill to give an EntityManager to an Entity object...
So I finally, I don't know what is the better way to check for a dupplicate entity inside a collection.
Of course, I could implement myself a thing like :
abstract class Cart implements InheritanceInterface{
...
public function addCartProduct(CartProduct $cartProduct): self
{
if(!$this->getCartProducts()->filter(
function (CartProduct $cp)use($cartProduct){
return $cp->getId() === $cartProduct->getId();
})->count()) {
$this->cartProducts->add($cartProduct);
$cartProduct->setCart($this);
}
return $this;
}
...
But it'll require to load every Entity and I really don't like the idea.
Personally I agree with your comment, I don't think the entity itself should have the responsibility to ensure there is no duplicate.
The entity cannot make a request like a repository could, and I don't see how you can be sure there is no duplicate in the database without querying it.
Calling contains will not trigger a fetch in your case, this means the collection will stay as is, which is not what you want anyway because you could have a previously persisted duplicate that will not be part of the collection because you marked it as EXTRA_LAZY.
You also don't want to fetch all the entities of the collection (and transform the results into objects) just to check if you have a collision.
So IMHO you should create a method in the repository of the entity to check for duplicates, a simple SELECT COUNT(id).
Then there is your real problem.
The way you make your test will never find a collision. When you do:
$cart = new ShoppingCart($this->createMock(ShoppingCartState::class));
$cart_product = new CartProduct();
$cart_product->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product);
$cart_product2 = new CartProduct();
$cart_product2->setProduct(new Product(self::NO_.'1', new Group('1')));
$cart->addCartProduct($cart_product2);
$this->assertCount(1, $cart->getCartProducts());
You are creating two instances of CartProduct, that's why the call to contains doesn't find anything.
Because contains checks for the object reference, not the content, like you can see in its implementation:
public function contains($element)
{
return in_array($element, $this->elements, true);
}
So in your test case what you're really testing is:
in_array(new CartProduct(), [new CartProduct()], true);
which will always return false.

Hint iterator of strict type entities in PHPDoc

Specifics
I am using PHPStorm 8 IDE.
Let's assume we have some class Foo which implements \Iterator interface and we know that all items inside that iterator will be of instance of class Bar.
Question
How to hint that Foo is iterable and contains only items of Bar? Of course, hint should keep information, that it's instance of Foo
What I tried so far
If we had an array of Bar instances, then that is an easy thing (it's described, for instance, in this question): Bar[]. Also, if the intention is to iterate through Foo, it still can be resolved (more or less) with:
//assume that $foo is instance of Foo
//..
/* #var $object Bar */
foreach ($foo as $object) {
}
However, there is one very important thing which is not achievable with in-place hinting: return type. If I'll have some method which should return Foo, I know only how to hint that Foo, but user of that function still won't be able to expose, that it's actually iterable and contains Bar instances (like it would be if I'll specify #return Bar[] in case with array of Bar instances)
If Foo implements Iterator then you can hint the return type on Foo::current(). PHPStorm will recognise that what Foo::current() returns is the value when you foreach over Foo.
For example:
<?php
class Foo implements Iterator
{
// ...
/**
* #return Bar
*/
public function current()
{
// ...
}
// ...
}
$foo = new Foo();
foreach ($foo as $object) {
// PHPStorm will recognise $object is type Bar.
}

Activerecord-association: create new object (find class)

I have an model with a relation, and I want to instantiate a new object of the relations type.
Example: A person has a company, and I have a person-object: now I
want to create a company-object.
The class of the companyobject is defined in the relation, so I don't think I should need to 'know' that class, but I should be able to ask the person-object to provide me with a new instance of type company? But I don't know how.
This is -I think- the same question as New model object through an association , but I'm using PHPActiveRecord, and not the ruby one.
Reason behind this: I have an abstract superclass person, and two children have their own relation with a type of company object. I need to be able to instantiate the correct class in the abstract person.
A workaround is to get it directly from the static $has_one array:
$class = $this::$has_one[1]['class_name'];
$company = new $class;
the hardcoded number can of course be eliminated by searching for the association-name in the array, but that's still quite ugly.
If there is anyone who knows how this is implemented in Ruby, and how the phpactiverecord implementation differs, I might get some Ideas from there?
Some testing has revealed that although the "search my classname in an array" looks kinda weird, it does not have any impact on performance, and in use it is functional enough.
You can also use build_association() in the relationship classes.
Simplest way to use it is through the Model's __call, i.e. if your relation is something like $person->company, then you could instantiate the company with $company = $person->build_company()
Note that this will NOT also make the "connection" between your objects ($person->company will not be set).
Alternatively, instead of build_company(), you can use create_company(), which will save a new record and link it to $person
In PHPActiveRecord, you have access to the relations array. The relation should have a name an you NEED TO KNOW THE NAME OF THE RELATIONSHIP/ASSOCIATION YOU WANT. It doesn't need to be the classname, but the classname of the Model you're relating to should be explicitly indicated in the relation. Just a basic example without error checking or gritty relationship db details like linking table or foreign key column name:
class Person extends ActiveRecord\Model {
static $belongs_to = array(
array('company',
'class_name' => 'SomeCompanyClass')
);
//general function get a classname from a relationship
public static function getClassNameFromRelationship($relationshipName)
foreach(self::$belongs_to as $relationship){
//the first element in all relationships is it's name
if($relationship[0] == $relationshipName){
$className = null;
if(isset($relationship['class_name'])){
$className = $relationship['class_name'];
}else{
// if no classname specified explicitly,
// assume the clasename is the relationship name
// with first letter capitalized
$className = ucfirst($relationship);
}
return $className
}
}
return null;
}
}
To with this function, if you have a person object and want an object defined by the 'company' relationship use:
$className = $person::getClassNameFromRelationship('company');
$company = new $className();
I'm currently using below solution. It's an actual solution, instead
of the $has_one[1] hack I mentioned in the question. If there is a
method in phpactiverecord I'm going to feel very silly exposing
msyelf. But please, prove me silly so I don't need to use this
solution :D
I am silly. Below functionality is implemented by the create_associationname call, as answered by #Bogdan_D
Two functions are added. You should probably add them in the \ActiveRecord\Model class. In my case there is a class between our classes and that model that contains extra functionality like this, so I put it there.
These are the 2 functions:
public function findClassByAssociation($associationName)
Called with the name of the association you are looking for.
Checks three static vars (has_many,belongs_to and has_one) for the association
calls findClassFromArray if an association is found.
from the person/company example: $person->findClassByAssociation('company');
private function findClassFromArray($associationName,$associationArray)
Just a worker-function that tries to match the name.
Source:
/**
* Find the classname of an explicitly defined
* association (has_one, has_many, belongs_to).
* Unsure if this works for standard associations
* without specific mention of the class_name, but I suppose it doesn't!
* #todo Check if works without an explicitly set 'class_name', if not: is this even possible (namespacing?)
* #todo Support for 'through' associations.
* #param String $associationName the association you want to find the class for
* #return mixed String|false if an association is found, return the class name (with namespace!), else return false
* #see findClassFromArray
*/
public function findClassByAssociation($associationName){
//$class = $this::$has_one[1]['class_name'];
$that = get_called_class();
if(isset($that::$has_many)){
$cl = $this->findClassFromArray($associationName,$that::$has_many);
if($cl){return $cl;}
}
if(isset($that::$belongs_to)){
$cl = $this->findClassFromArray($associationName,$that::$belongs_to);
if($cl){return $cl;}
}
if(isset($that::$has_one)){
$cl = $this->findClassFromArray($associationName,$that::$has_one);
if($cl){return $cl;}
}
return false;
}
/**
* Find a class in a php-activerecord "association-array". It probably should have a specifically defined class name!
* #todo check if works without explicitly set 'class_name', and if not find it like standard
* #param String $associationName
* #param Array[] $associationArray phpactiverecord array with associations (like has_many)
* #return mixed String|false if an association is found, return the class name, else return false
* #see findClassFromArray
*/
private function findClassFromArray($associationName,$associationArray){
if(is_array($associationArray)){
foreach($associationArray as $association){
if($association['0'] === $associationName){
return $association['class_name'];
}
}
}
return false;
}

Categories