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.
Related
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.
Long story short.
I use Doctrine's Single Table Inheritance mapping to map three different contexts (classes) of the one common entity: NotActivatedCustomer, DeletedCustomer, and Customer. Also, there is an AbstractCustomer which contains the next:
App\Identity\Domain\Customer\AbstractCustomer:
type: entity
inheritanceType: SINGLE_TABLE
discriminatorColumn:
name: discr
type: string
discriminatorMap:
Customer: App\Identity\Domain\Customer\Customer
NotActivatedCustomer: App\Identity\Domain\Customer\NotActivatedCustomer
DeletedCustomer: App\Identity\Domain\Customer\DeletedCustomer
table: customer
id:
id:
type: customer_id
unique: true
generator:
strategy: CUSTOM
customIdGenerator:
class: Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator
fields:
email:
type: email
length: 180
unique: true
A Subtype definition example:
<?php
declare(strict_types=1);
namespace App\Identity\Domain\Customer;
use App\Identity\Domain\User\Email;
class DeletedCustomer extends AbstractCustomer
{
public const TYPE = 'DeletedCustomer';
public function __construct(CustomerId $id)
{
$this->_setId($id);
$this->_setEmail(new Email(sprintf('%s#mail.local', $id->value())));
}
}
The Use Case:
<?php
declare(strict_types=1);
namespace App\Identity\Application\Customer\UseCase\DeleteCustomer;
use App\Identity\Application\Customer\CustomerEntityManager;
use App\Identity\Application\User\AuthenticatedCustomer;
use App\Identity\Domain\Customer\DeletedCustomer;
use App\Shared\Application\ImageManager;
final class DeleteCustomerHandler
{
private CustomerEntityManager $customerEntityManager;
private AuthenticatedCustomer $authenticatedCustomer;
private ImageManager $imageManager;
public function __construct(AuthenticatedCustomer $authenticatedCustomer,
CustomerEntityManager $customerEntityManagerByActiveTenant,
ImageManager $customerPhotoManager)
{
$this->customerEntityManager = $customerEntityManagerByActiveTenant;
$this->authenticatedCustomer = $authenticatedCustomer;
$this->imageManager = $customerPhotoManager;
}
public function handle(): void
{
$customer = $this->authenticatedCustomer->customer();
$photo = (string) $customer->photo();
$deletedCustomer = new DeletedCustomer($customer->id());
// TODO OR return DeletedCustomer that way
// $deletedCustomer = $customer->deactive();
// entityManager->merge() called here
$this->customerEntityManager->sync($deletedCustomer);
// simple entityManager->flush() under the hood
$this->customerEntityManager->update();
// that's a raw query to update discriminator field, hackish way I'm using
// UPDATE customer SET discr = ? WHERE id = ?
$this->customerEntityManager->updateInheritanceType($customer, DeletedCustomer::TYPE);
if ($photo) {
$this->imageManager->remove($photo);
}
}
}
So if you have already an existing Customer persisted and run DeleteCustomerHandler, the Customer will be updated, but its discriminator field won't!
Googling that, there is no way to update the discriminator field not going some hackish way like I do (running raw query manually to update the field).
Also, I need to use the EntityManager->merge() method to add manually initialized DeletedCustomer to internal UnitOfWork. Looks a little bit dirty too, and it's a deprecated method for Doctrine 3, so the question also is there a better way to handle my case?
So, to conclude all the questions:
Am I doing Customer's status change to DeletedCustomer completely wrong? I'm just trying to avoid Customer God Object, distinguish this Entity's bounded contexts, kinda that.
How to avoid EntityManager->merge() there? AuthenticatedCustomer comes from session (JWT).
I think you're absolutely right to want to avoid Customer turning into a god object. Inheritance is one way to do it, but using it for customers in different statuses can lead to problems.
The two key problems in my experience:
As new statuses emerge, will you keep adding different inherited entities?
What happens when you have a customer move through two different statuses, such as a customer that was a NotActivatedCustomer but is now a DeletedCustomer?
So I keep inheritance only when the inherited type is genuinely more specific type, where a given entity will only ever be one of those types for its entire lifecycle. Cars don't become motorbikes, for example.
I have two patterns for solving the problem differently to you. I tend to start with the first and move to the second.
interface DeletedCustomer
{
public function getDeletedAt(): DateTime;
}
interface NotActivatedCustomer
{
public function getCreatedAt(): DateTime;
}
class Customer implements DeletedCustomer, NotActivatedCustomer
{
private $id;
private $name;
private DateTime $deletedAt;
private bool $isActivated = false;
public function getDeletedAt(): DateTime {...}
public function getCreatedAt(): DateTime {...}
}
class DeletedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.deletedAt IS NOT NULL
>>>)->getQuery()->getResults();
}
}
class NotActivatedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.isActivated = false
>>>)->getQuery()->getResults();
}
}
class DeletedCustomerService
{
public function doTheThing(DeletedCustomer $customer) {}
}
This reduces coupling, which is one of the main problems with god objects. So when the columns start to proliferate, I can move them off to real entities that join to the Customer. Components that refer to DeletedCustomer will still receive one.
The second pattern is event-sourcing-lite - have a many-to-one relationship with a "CustomerLifecycleEvent" entity. Query based on whether the customer has a "deleted" event. This second approach is much more complex, both to update and query. You can still have dedicated repositories that return entities like DeletedCustomer, but you'll need to do a bit more boilerplate.
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());
}
}
I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps
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;
}