PHPStan cast to more specific array shape - phpstan

In my code, I call a method whose return value is typed as follows:
/**
* #return array<int, array<string,mixed>>
*/
public function fetchAllAssociative(): array;
The subtype array<string, mixed> describes the database record in a generic way. I know exactly what this database record looks like so I want to be more specific in documenting the method:
/**
* #return array<int, array{id: int, firstName: string, lastName: string}>
*/
public function getAllUsers(): array
{
return $this->userRepository->fetchAllAssociative();
}
However, PHPStan complains that I should type my methods exactly as fetchAllAssociative:
Method UserRepository::getAllUsers() should return array<int, array{id: int, firstName: string, lastName: string}> but returns array<int, array<string, mixed>>.
Is there a way to cast it? My workaround is to introduce a new variable and use the #var tag but I don't really like it (other static code analysis tools don't like it as well)
/**
* #return array<int, array{id: int, firstName: string, lastName: string}>
*/
public function getAllUsers(): array
{
/**
* #var array<int, array{id: int, firstName: string, lastName: string}> $data
*/
$data = $this->userRepository->fetchAllAssociative();
return $data;
}

Related

Filtering a Collection using Criteria targeting an Embeddable object

Let's say you have a class Title, and the title is translated in multiple languages using TitleTranslation classes.
To indicate which language the title is translated in, each translation has a Locale value object.
For readability, I am attempting to provide the Title class with a getTitle(Locale $locale) method, returning the correct translation.
The easy way to do this would be to loop over all translations, and check each translation's locale.
However, I would like to accomplish this using Criteria, so only a single TitleTranslation will be fetched and hydrated.
To illustrate the case, a simplified version of the classes I'm working with:
Title:
/** #ORM\Entity #ORM\Table */
class Title
{
/**
* #ORM\OneToMany(targetEntity="TitleTranslation", mappedBy="element")
*/
private $translations;
}
TitleTranslation:
/** #ORM\Entity #ORM\Table */
class TitleTranslation
{
/**
* #ORM\ManyToOne(targetEntity="Title", inversedBy="translations")
*/
private $title;
/**
* #ORM\Column(type="string")
*/
private $translation;
/**
* #ORM\Embedded(class="Locale")
*/
private $locale;
public function getTranslation() : string
{
return $this->translation;
}
}
Locale:
/** #Embeddable */
class Locale
{
/** #ORM\Column(type="string")
private $locale;
public function __toString()
{
return $this->locale;
}
}
I have made the following attempts, all of which are unsuccessful:
public function getTitle(Locale $locale)
{
$localeCriteria = Criteria::create()->where(Criteria::expr()->eq('locale', $locale));
/** #var TitleTranslation | bool $translation */
$translation = $this->translations->matching($translationCriteria)->first();
return $translation ? $translation->getTranslation() : null;
}
This approach fails with an ORMException "Unrecognized field: locale", which seems normal, as Embeddables should be queried as "locale.locale" (field in containing class.field in VO).
However, using this notation:
$localeCriteria = Criteria::create()->where(Criteria::expr()->eq('locale.locale', $locale));
Fails with Undefined property: TitleTranslation::$locale.locale
Am I missing something, or is this approach simply not possible?

Map a discriminator column to a field with Doctrine 2

In my project I have several class table inheritances like this:
namespace MyProject\Model;
/**
* #Entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/** #Entity */
class Employee extends Person
{
// ...
}
I have a method which converts entities to arrays based on the fields which have public getters. The problem here is that I lose the inheritance information in my array because the discriminator value isn't stored in a field.
So what I tried was the following, hoping doctrine would automatically set $disc:
class Person
{
// can I automatically populate this field with 'person' or 'employee'?
protected $discr;
public function getDiscr() { return $this->discr; }
public function setDiscr($disc) { $this->discr; }
// ...
}
Is there a way to make this work in doctrine? Or would I need to read the class metadata in my entity-to-array method?
Sadly, there is no documented way to map the discr column to an entity. That's because the discr column is really part of the database and not the entity.
However, it's quite common to just put the discr value directly in your class definition. It's not going to change and you will always get the same class for the same value anyways.
class Person
{
protected $discr = 'person';
class Employee extends Person
{
protected $discr = 'employee';
Here's a small example of what I have in one of my ZF2 projects (using Doctrine MongoDB ODM):
// an instance of your entity
$entity = ...;
/** #var \Doctrine\ODM\MongoDB\DocumentManager $documentManager */
$documentManager = $serviceManager->get('DocumentManager');
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory $factory */
$factory = $documentManager->getMetadataFactory()
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $metadata */
$metadata = $factory->getMetadataFor(get_class($object));
if ($metadata->hasDiscriminator()) {
// assuming $data is result of the previous extraction
$data[$metadata->discriminatorField] = $metadata->discriminatorValue;
}
What I have done is I've implemented a custom interface DiscriminatorAwareInterface and I only apply the checks to classes that implement it (in your case it would be the class that all "discriminated" classes extend.
As a result I end up with code that looks like this:
// add value of the discrinimator field to entities that support it
if ($object instanceof DiscriminatorAwareInterface) {
/** #var \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $metadata */
$metadata = $factory->getMetadataFor(get_class($object));
if ($metadata->hasDiscriminator()) {
$data[$metadata->discriminatorField] = $metadata->discriminatorValue;
}
}
I'm pretty sure it will be the same if you use the standard ORM, except instead of a document manager you will have entity manager.
Just got this problem and solved it without defining the discriminator as a real member:
abstract class MyEntity {
const TYPE_FOO = 'foo';
const TYPE_BAR = 'bar';
const TYPE_BUZ = 'buz';
...
/**
* #return string
*/
public function getMyDiscriminator()
{
$myDiscriminator = null;
switch (get_class($this)) {
case MyEntityFoo::class:
$myDiscriminator = self::TYPE_FOO;
break;
case MyEntityBar::class:
$myDiscriminator = self::TYPE_BAR;
break;
case MyEntityBuz::class:
$myDiscriminator = self::TYPE_BUZ;
break;
}
return $myDiscriminator;
}
...
}
class MyEntityFoo extends MyEntity {}
class MyEntityBar extends MyEntity {}
class MyEntityBuz extends MyEntity {}
You can use the following solution:
`$`$metadata = \Doctrine\ORM\Mapping\ClassMetadata((string)$entityName);
print_r($metadata->discriminatorValue);`

Doctrine2: How to use array of objects in array type?

Doctrine2 has a built-in type 'array' that I find useful for my project. It perfectly works with arrays of scalar types. But now I want to use an array of objects. Something like this:
/**
* #ORM\Entity
*/
class MyEntity {
/**
* #var MyEntityParameter[] array of MyEntityParameter instances
*
* #ORM\Column(name="parameters", type="array", nullable=true)
*
*/
private $parameters;
}
Where MyEntityParameter is a class that can be serialized. I also use it in the Symfony's Form Builder.
My plan works perfectly, except that when the field in the MyEntityParameter instance gets changed, Doctrine doesn't detect it and thus doesn't update the record. If I delete or add array elements, Doctrine detects that. I realize that this happens because class instance object id doesn't change when I change its field, but then how can I make it so that Doctrine detects this change?
I found a working solution for me. I don't think it's that elegant, but in case if there are no good ways to solve this problem it can work for me and for others.
First of all, I decided not to keep objects in the array, but instead keep arrays. I, however, still want to use MyEntityParameter class in the Symfony's form builder. In this case, the idea is to disable mapping for our field:
In the form builder we do the following:
// Acme/Bundle/DemoBundle/Form/Type/MyEntityType.php
// ...
class MyEntityType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parameters', 'collection', array(
'mapped' => false, // do not map this field
'type' => new MyEntityParameterType(),
// ... other options ...
));
}
}
In the child type (MyEntityParameterType) we set 'data_class' to MyEntityParameter (I don't show the code, as it's not related to the problem).
Now all we need is to manually fill and process the data for this not-mapped field.
In the controller:
public function editAction($id, Request $request)
{
// ...
// $object is an instance of MyEntity
$form = $this->createForm(new MyEntityType(), $object);
$parameters = $object->getParameters();
if ($parameters) {
foreach ($parameters as $key => $parameter)
{
$form->get('parameters')->add($key, new MyEntityParameterType(),
array(
// here I assume that the constructor of MyEntityParameter
// accepts the field data in an array format
'data' => new MyEntityParameter($parameter),
)
);
}
}
if ($request->isMethod('POST')) {
$form->submit($request);
$parameters = array();
foreach ($form->get('parameters')->all() as $parameter) {
// here first getData() gives MyEntityParameter instance
// and the second getData() is just a method of MyEntityParameter
// that returns all the fields in an array format
$parameters[] = $parameter->getData()->getData();
}
$object->setParameters($parameters);
// if the parameters were changed in the form,
// this change will be detected by UnitOfWork
}
// ...
}
I remember coming across this issue before, and a workaround that worked for me was to set a new object so Doctrine would recognise the entity property has been modified, then set the object that has the changes you want persisted.
$parameters = $entity->getParameters();
$parameters->foo = "bar";
$entity->setParameters(new MyEntityParameter());
$entity->setParameters($parameters);
$em->persist($entity);
If it's an array of MyEntityParameter instances then the following code might work.
$parameters = $entity->getParameters();
$parameters[3]->foo = "bar"; // Just an example
$entity->setParameters(array(new MyEntityParameter()));
$entity->setParameters($parameters);
$em->persist($entity);

Doctrine 2 Hydrator - fields with underline

I working with zf2.1.3 and doctrine 2. I was trying to hydrate information on my class and realize that the DoctrineModule\Stdlib\Hydrator\DoctrineObject doesn't work with fields that have underline on it, like cat_id.
Here an example:
/* namespace Application\Entity; */
class Foo
{
private $cat_id;
private $cat_name;
public function getCatId()
{
return $this->cat_id;
}
public function setCatName($name)
{
$this->cat_name = $name;
return $this;
}
public function getCatName()
{
return $this->cat_nome;
}
}
class Bar
{
private $id;
private $name;
public function getId()
{
return $this->id;
}
public function setName($name)
{
$this->name = $name;
return $this;
}
public function getName()
{
return $this->nome;
}
}
/* namespace Application\Controller; */
use \DoctrineModule\Stdlib\Hydrator\DoctrineObject;
public function indexAction()
{
$hydrator = new DoctrineObject($this->getEntityManager(), 'Application\Entity\Foo');
$foo = $hydrator->hydrate(array('cat_name' => 'Frank Moraes'), new Foo());
\Zend\Debug\Debug::dump($foo, 'Foo Hydrator');
$hydrator = new DoctrineObject($this->getEntityManager(), 'Application\Entity\Bar');
$bar = $hydrator->hydrate(array('name' => 'Frank Moraes'), new Bar());
\Zend\Debug\Debug::dump($inscrit, 'Bar Hydrator');
}
This code returns the following:
Foo Hydrator
object(Application\Entity\Foo)
private 'cat_id' => null
private 'cat_name' => null
Bar Hydrator
object(Application\Entity\Foo)
private 'id' => null
private 'name' => 'Frank Moraes'
So my question is: Why Doctrine Hydrator doesn't work with fields that have underline in it?
How can I make this work?
Thank you!
Edited
Sorry for the long time with no answer. A have no access to SO on my work!
I tried the following:
$hydrator = new DoctrineObject($this->getEntityManager(), 'Application\Entity\Foo', false);
For the example I posted here, this false parameter works fine.
But, it didn't work when I'm binding the class on a form!
Someone have a clue?
Several months after this has been asked, but I've been checking the source code of the DoctrineObject hydrator just now, and I think this is what's going on:
By default, unless you construct the DoctrineObject hydrator with the byValue flag as false, the hydrator will work in byValue mode. What that means is that it tries to construct getter and setter method names from the values that you're trying to hydrate, and the way it does that is by calling ucfirst on the field name and prepending get/set to that.
So, for instance, you have cat_name, so it will try the getter method getCat_name which clearly is incorrect.
You have 4 choices, then:
A: camelCase your variable names
B: Set byValue to false (so that it tries to access the variables directly) [although I think you might have to make the variables public in that case... I'm not sure how visibility will affect it, as I haven't tried it before]
C: Use a different hydration Strategy or
D: Just have weird getter and setter names like getCat_name (please don't do this).
Quick and dirty...
foreach ($form as $key => $value) {
while ($pos = strrpos($key,'_')) {
$key = substr_replace($key, '', $pos, 1);
$capitalized = strtoupper($key[$pos]);
$key[$pos] = $capitalized;
}
$data[$key] = $value;
}
You can still use this trick:
/** #ORM\Column(name="column_name", type="string") */
protected $columnName;
function get(...);
function set($columnName){$this->columnName = $columnName}
Hope helps

Doctrine 2 Can't Seem to Remove Many to Many Relationships

I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...