I'm running into unexpected behavior in CakePHP 3.x when using collection filters. When I provide a collection of type Cake\Collection\Collection, the filter returns a new collection with type Cake\Collection\Iterator\FilterIterator.
Here is some sample code that demonstrates the behavior:
$people = [
['name' => 'Alice', 'age' => 17],
['name' => 'Bob', 'age' => 51]
];
$collection = new Collection($people);
$adults = $collection->filter(function ($person, $key) {
return $person['age'] > 17;
});
The output of debuging $collection and $adults shows the type change:
object(Cake\Collection\Collection) {
'count' => (int) 2
}
object(Cake\Collection\Iterator\FilterIterator) {
'count' => (int) 1
}
This object type change is causing unexpected problems in my code. I've resolved the issue by rebuilding the collection after each filter. For example:
$adults = new Collection($adults->toArray());
This seems a little ugly. Is there a better method for retaining the Cake\Collection\Collection type? Any advice appreciated!
I am new to Symfony, i am facing an issue when i tried to insert a record. Here is part of my code at FormRequestDao.php:
public function saveFormRequest(FormRequest $formRequest, $formList, $entitlements) {
$conn = Doctrine_Manager::connection();
$conn->beginTransaction();
print_r($formRequest);
$formRequest->save();
...
return $formRequest;
}
I always get the value "note" in null/blank, it just a normal textbox.
Here is the partial result of print_r($formRequest):
[_data:protected] => Array
(
[id] => Doctrine_Null Object
(
)
[product_type_id] => 1
[date_inserted] => 2017-05-31
[item_number] => 0002
[description] => This is a product 1.
[note] => Doctrine_Null Object
(
)
)
And the value of "note" has been captured in productApplicationService.php:
public function insertProduct(ProductParameterObject $productAssignmentData) {
print_r($productAssignmentData);
return $this->saveFormRequest($productAssignmentData);
}
Any clue for me?
Thanks.
If you need a response for your twig file you need to return this for example
return $this->render(':yourtwig.twig', [
'yourEntity' => $this->saveFormRequest($productAssignmentData)
]);
Subject
When I have a set of entities with a Doctrine Discriminator Map then I cannot add a filter to get just one type of all mapped entities, due SonataAdminBundle and/or SonataDoctrineORMAdminBundle are throwing an error.
Example:
Entities with a Doctrine Discriminator Map
/**
* #ORM\Table(name="activities")
* #ORM\Entity()
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "joined" = "...\JoinedActivity",
* "other" = "...\OtherActivity"
* })
*/
abstract class Activity()
{
abstract public function getType();
}
/**
* #ORM\Entity()
*/
class JoinActivity extends Activity()
{
const TYPE = 'joined';
public function getType()
{
return self::type;
}
}
/**
* #ORM\Entity()
*/
class OtherActivity extends Activity()
{
const TYPE = 'other';
public function getType()
{
return self::type;
}
}
Then I add the Sonata Admin filter:
protected function configureDatagridFilters(DatagridMapper $filter)
{
$filter->add(
'type',
null,
[
'label' => 'Activity Type',
],
'choice',
[
'choices' => [
JoinActivity::TYPE => ucfirst(JoinActivity::TYPE),
OtherActivity::TYPE => ucfirst(OtherActivity::TYPE),
],
]
);
}
Expected results
Get a new filter to select just joined or other activities.
Actual results
Notice: Undefined index: type
500 Internal Server Error - ContextErrorException
Stack trace
As requested by greg0ire this is the Stack Trace returned by Symfony/Sonata:
[1] Symfony\Component\Debug\Exception\ContextErrorException: Notice: Undefined index: type
at n/a
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php line 69
at Symfony\Component\Debug\ErrorHandler->handleError('8', 'Undefined index: type', '/path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php', '69', array('class' => 'AppBundle\EntityBundle\Entity\Activity', 'property' => 'type', 'modelManager' => object(ModelManager), 'ret' => array(object(ClassMetadata), 'type', array()), 'options' => array('field_type' => null, 'field_options' => array(), 'options' => array(), 'parent_association_mappings' => array()), 'metadata' => object(ClassMetadata), 'propertyName' => 'type', 'parentAssociationMappings' => array()))
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Guesser/FilterTypeGuesser.php line 69
at Sonata\DoctrineORMAdminBundle\Guesser\FilterTypeGuesser->guessType('AppBundle\EntityBundle\Entity\Activity', 'type', object(ModelManager))
in /path/to/symfony/project/app/cache/dev/classes.php line 15104
at Sonata\AdminBundle\Guesser\TypeGuesserChain->Sonata\AdminBundle\Guesser\{closure}(object(FilterTypeGuesser))
in /path/to/symfony/project/app/cache/dev/classes.php line 15111
at Sonata\AdminBundle\Guesser\TypeGuesserChain->guess(object(Closure))
in /path/to/symfony/project/app/cache/dev/classes.php line 15105
at Sonata\AdminBundle\Guesser\TypeGuesserChain->guessType('AppBundle\EntityBundle\Entity\Activity', 'type', object(ModelManager))
in /path/to/symfony/project/vendor/sonata-project/doctrine-orm-admin-bundle/Builder/DatagridBuilder.php line 105
at Sonata\DoctrineORMAdminBundle\Builder\DatagridBuilder->addFilter(object(Datagrid), null, object(FieldDescription), object(ActivityAdmin))
in /path/to/symfony/project/app/cache/dev/classes.php line 13069
at Sonata\AdminBundle\Datagrid\DatagridMapper->add('type', null, array('label' => 'Activity Type', 'field_options' => array('choices' => array('joined' => 'Joined')), 'field_type' => 'choice', 'field_name' => 'type'), 'choice', array('choices' => array('joined' => 'Joined')))
in /path/to/symfony/project/src/AppBundle/SonAdminBundle/Admin/ActivityAdmin.php line 64
at AppBundle\SonAdminBundle\Admin\ActivityAdmin->configureDatagridFilters(object(DatagridMapper))
in /path/to/symfony/project/app/cache/dev/classes.php line 10609
at Sonata\AdminBundle\Admin\AbstractAdmin->buildDatagrid()
in /path/to/symfony/project/app/cache/dev/classes.php line 10910
at Sonata\AdminBundle\Admin\AbstractAdmin->getDatagrid()
in /path/to/symfony/project/vendor/sonata-project/admin-bundle/Controller/CRUDController.php line 104
at Sonata\AdminBundle\Controller\CRUDController->listAction()
in line
at call_user_func_array(array(object(CRUDController), 'listAction'), array())
in /path/to/symfony/project/app/bootstrap.php.cache line 3222
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), '1')
in /path/to/symfony/project/app/bootstrap.php.cache line 3181
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), '1', true)
in /path/to/symfony/project/app/bootstrap.php.cache line 3335
at Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel->handle(object(Request), '1', true)
in /path/to/symfony/project/app/bootstrap.php.cache line 2540
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
in /path/to/symfony/project/web/app_dev.php line 15
at require('/path/to/symfony/project/web/app_dev.php')
in /path/to/symfony/project/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/router_dev.php line 40
Any idea how I can fix it?
Thanks,
I face the same issue. I did a work around - I use doctrine_orm_callback type with doctrine INSTANCE OF operator.
Code looks like this:
->add('userType',
'doctrine_orm_callback',
[
'callback' => function ($queryBuilder, $alias, $field, $value) {
if (!is_array($value) || !array_key_exists('value', $value) || empty($value['value'])) {
return false;
}
$queryBuilder->andWhere($alias . ' INSTANCE OF :userType');
$queryBuilder->setParameter('userType', $value['value']);
return true;
},
],
ChoiceType::class,
[
'choices' => array_flip(UserType::getChoices()),
'translation_domain' => $this->getTranslationDomain(),
]
)
And it's working. Maybe it helps you.
I think Sonata is confused b/c it expects type to appear in the fields in your Doctrine mapping. I don't think filtering by type is supporting, but I recall having some support for single inheritance, especially when creating new objects. Can't find it back though.
It is not recommanded to modify the link between an inherited entity and his superclass once it is created. If you need it, you should consider using composition instead.
That's why Doctrine does not allow to directly manage the discriminator field.
All the following text is copied from this very useful response which is precious for a global and better understanding.
It is not a good sign when the type of an instance of an object needs to change over time. I'm not talking about downcasting/upcasting here, but about the need to change the real type of an object.
First of all, let me tell you why it is a bad idea:
A subclass might define more attributes and do some additionnal work
in it's constructor. Should we run the new constructor again? What
if it overwrites some of our old object's attributes?
What if you were working on an instance of that Person in some part of your code, and then it suddenly transforms into an Employee (which might have some redefined behavior you wouldn't expect)?!
That is part of the reason why most languages will not allow you to change the real class type of an object during execution (and memory, of course, but I don't want to get into details). Some let you do that (sometimes in twisted ways, e.g. the JVM), but it's really not good practice!
More often than not, the need to do so lies in bad object-oriented design decisions.
For those reasons, Doctrine will not allow you to change the type of your entity object. Of course, you could write plain SQL (at the end of this post - but please read through!) to do the change anyway, but here's two "clean" options I would suggest:
I realize you've already said the first option wasn't an option but I spent a while writing down this post so I feel like I should make it as complete as possible for future reference.
Whenever you need to "change the type" from Person to Employee, create a new instance of the Employee and copy the data you want to copy over from the old Person object to the Employee object. Don't forget to remove the old entity and to persist the new one.
Use composition instead of inheritance (see this wiki article for details and links to other articles). EDIT: For the hell of it, here's a part of a nice conversation with Erich Gamma about "Composition over Inheritance"!
See related discussions here and here.
Now, here is the plain SQL method I was talking about earlier - I hope you won't need to use it!
Make sure your query is sanitized (as the query will be executed without any verification).
$query = "UPDATE TABLE_NAME_HERE SET discr = 'employee' WHERE id = ".$entity->getId();
$entity_manager->getConnection()->exec( $query );
Here is the documentation and code for the exec method which is in the DBAL\Connection class (for your information):
/**
* Execute an SQL statement and return the number of affected rows.
*
* #param string $statement
* #return integer The number of affected rows.
*/
public function exec($statement)
{
$this->connect();
return $this->_conn->exec($statement);
}
I have defined a custom Doctrine data type for Uuid. When I search for an object using find($uuid), it works correctly, i.e. the attribute is converted using convertToDatabaseValue() before executing the query, and converted back with convertToPhpValue() when value is retrieved.
The conversion doesn't work if I use the QueryBuilder. Example:
$qb = $this->createQueryBuilder('s');
$qb = $qb->where( //some conditions...
$qb->expr()->eq( 's.uuid', ':uuid' ))->setParameter( 'uuid', $uuid );
I found two similar unanswered questions:
Symfony Doctrine datatype only works in findBy not querybuilder
Doctrine 2 Custom Types
It looks like that the conversion is in fact ignored.
How can I force the conversion of the parameter before executing the query? Is there a way to access the convertToDatabaseValue() function of the custom data type from the repository?
Thanks
Yes setParameter() has third parameter, but the type of third param as string is worked for me not the object.
You can do it in following way.
$qb = $this->createQueryBuilder('s');
$qb = $qb->where( //some conditions...
$qb->expr()->eq( 's.uuid', ':uuid' ))->setParameter( 'uuid', $uuid, 'uuid' );
If you dont know what exactly key is for datatype 'uuid' is.
Then use print_r(Type::getTypesMap()); to get list of all dataypes added.
In my case it was
Array
(
[array] => Doctrine\DBAL\Types\ArrayType
[simple_array] => Doctrine\DBAL\Types\SimpleArrayType
[json_array] => Doctrine\DBAL\Types\JsonArrayType
[object] => Doctrine\DBAL\Types\ObjectType
[boolean] => Doctrine\DBAL\Types\BooleanType
[integer] => Doctrine\DBAL\Types\IntegerType
[smallint] => Doctrine\DBAL\Types\SmallIntType
[bigint] => Doctrine\DBAL\Types\BigIntType
[string] => Doctrine\DBAL\Types\StringType
[text] => Doctrine\DBAL\Types\TextType
[datetime] => Doctrine\DBAL\Types\DateTimeType
[datetimetz] => Doctrine\DBAL\Types\DateTimeTzType
[date] => Doctrine\DBAL\Types\DateType
[time] => Doctrine\DBAL\Types\TimeType
[decimal] => Doctrine\DBAL\Types\DecimalType
[float] => Doctrine\DBAL\Types\FloatType
[binary] => Doctrine\DBAL\Types\BinaryType
[blob] => Doctrine\DBAL\Types\BlobType
[guid] => Doctrine\DBAL\Types\GuidType
[geometry] => CrEOF\Spatial\DBAL\Types\GeometryType
[point] => CrEOF\Spatial\DBAL\Types\Geometry\PointType
[polygon] => CrEOF\Spatial\DBAL\Types\Geometry\PolygonType
[linestring] => CrEOF\Spatial\DBAL\Types\Geometry\LineStringType
)
And my doctrine code was something like this.
$queryBuilder = $this->createQueryBuilder('c');
$queryBuilder
->where('st_contains(:polygon, point(c.latitude, c.longitude) ) = 1')
->setParameter('polygon', $city->getPolygon(), 'polygon');
Here's the solution: the function setParameter() has a third argument $type which is used to declare the typology of the parameter. The custom declared type can be retrieved with the getType() function of the Doctrine Type class:
$qb = $this->createQueryBuilder('s');
$qb = $qb->where( //some conditions...
$qb->expr()->eq( 's.uuid', ':uuid' ))->setParameter( 'uuid', $uuid, Type::getType('uuid') );
I'm trying to do a custom sort in ModelAdmin on SiteTree object
public function getList() {
$list = parent::getList();
if($this->modelClass == 'Listing') {
$list = $list->sort(array('Status' => 'ASC','Street' => 'ASC'));
}
return $list;
}
The sort works but when you try to edit a record I get the following error:
[User Error] Couldn't run query: SELECT DISTINCT "SiteTree_Live"."ID", "Status", "Street" FROM "SiteTree_Live" WHERE ("SiteTree_Live"."ClassName" IN ('Listing','UnavailableListing')) ORDER BY "Status" ASC, "Street" ASC LIMIT 30 Unknown column 'Status' in 'field list'
Obviously Status doesn't exist on SiteTree, it exists on Listing.
You've kind of answered your own question. You can't order by Status when querying SiteTree if the column doesn't exist on that table.
You need to change your model class to Listing or add the Status column to your SiteTree table.