I have an entity, Offer, with a ManyToMany relationship to a Country Entity (i.e. an offer can be available on many countries)
Class Offer
{
[...]
/**
* #ORM\ManyToMany(targetEntity="Country")
* #ORM\JoinTable(
* name="offer_cc",
* joinColumns={
* #ORM\JoinColumn(name="offer_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="cc", referencedColumnName="cc")
* }
* )
*/
private $countries;
}
And I want to query for all offers which have a country of a given array.
An image to better understand:
In this case, It should show only Offer 1 and Offer 2 because the former has Andorra and the latter has Italy.
From the form I have an ArrayCollection of Country entities.
Is it possible to do this in a EntityRepository with a query builder?
For example, this is how I filter by payoutMode, which is a simple int value:
class OfferRepository extends EntityRepository
{
public function findAllFiltered(array $filter = [])
{
$qb = $this->createQueryBuilder('offer');
// Show only active offers
$qb->where('offer.status=1');
if($filter['payoutMode'] ?? null) {
$qb->andWhere("offer.payoutMode = :payoutMode")->setParameter(':payoutMode', $filter['payoutMode']);
}
// TODO add filter by cc, category, tags
return $qb->getQuery()->execute();
}
}
Here, $filter['countries'] contains:
ArrayCollection {#748 ▼
-elements: array:2 [▼
0 => Country {#762 ▼
-cc: "AD"
}
1 => Country {#769 ▼
-cc: "IT"
}
]
}
In DQL you there is IN function which can takes as right-side argument an array of IDs or array of entities. Use it for JOIN condition
Since you already get ArrayList of Country entities, it could be something like this:
class OfferRepository extends EntityRepository
{
public function findAllFiltered(array $filter = [])
{
$qb = $this->createQueryBuilder('offer');
// Show only active offers
$qb->where('offer.status=1');
if($filter['payoutMode'] ?? null) {
$qb->join("offer.payoutMode = :payoutMode")->setParameter(':payoutMode', $filter['countries']);
}
if(!empty($filter['payoutMode'])) {
$qb->join('offer.countries', 'c', Expr\Join::WITH, 'c IN :countries')
->setParameter(':countries', $filter['countries']);
}
// TODO add filter by cc, category, tags
return $qb->getQuery()->execute();
}
}
Code not tested, so I could mess something up with the DQL JOIN syntax.
I'm not sure if 'c IN :countries' syntax in valid in this context.
But in general, this is the way.
I think I found the solution:
if(count($filter['countries'] ?? [])) {
$qb->leftJoin('offer.countries', 'countries')
->andWhere("countries IN (:ccs)")->setParameter(':ccs', $filter['countries']);
}
Related
I have a Symfony rest api build with fos restbundle and I'm deserializing a json PUT request in order to update a doctrine entity with a to-many relation.
However, the to-many child objects which are configured with orphanremoval=true does not get removed from the database when they are not present in the json data.
The PUT request payload:
{
"id": 1,
"name":"Some name",
"export_destinations": [
{
"id": 1,
"type": "USER_STORAGE",
"user": {"id": 5}
}
{
"id": 2,
"type": "SYSTEM_STORAGE"
}
]
}
The controller action:
/**
* #Rest\Put("{id}")
* #ParamConverter(
* "exportJob",
* converter="fos_rest.request_body",
* options={"deserializationContext"={"groups"={"put"}}}
* )
* #Rest\View(serializerGroups={"details"})
* #param ExportJob $exportJob
* #return ExportJob
*/
public function putAction(ExportJob $exportJob)
{
$this->getManager()->persist($exportJob);
$this->getManager()->flush();
return $exportJob;
}
ExportJob entity
/**
* #ORM\Entity()
*/
class ExportJob
{
/**
* #var ArrayCollection|ExportDestination[]
*
* #ORM\OneToMany(targetEntity="ExportDestination", mappedBy="exportJob", cascade={"persist", "remove", "merge"}, orphanRemoval=true)
*/
protected $exportDestinations;
/**
* #param ExportDestination $exportDestination
* #return $this
*/
public function addExportDestination(ExportDestination $exportDestination)
{
$exportDestination->setExportJob($this);
$this->exportDestinations->add($exportDestination);
return $this;
}
/**
* #param ExportDestination $exportDestination
* #return $this
*/
public function removeExportDestination(ExportDestination $exportDestination)
{
$this->exportDestinations->removeElement($exportDestination);
$exportDestination->setExportJob(null);
return $this;
}
}
JMS meta data
MyProject\ExportBundle\Entity\ExportJob:
exclusion_policy: ALL
properties:
id:
groups: ['list', 'details', 'put']
expose: true
name:
groups: ['list', 'details', 'put', 'patch', 'post']
expose: true
exportDestinations:
groups: ['details', 'put', 'patch', 'post']
expose: true
type: 'ArrayCollection<MyProject\ExportBundle\Entity\ExportDestination>'
I am using the DoctrineObjectConstructor
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: false
Now when I leave out the second object from the export_destinations array in the json payload my exportJob in the controller action has only one exportDestination object in the array collection after deserialization.
But when I persist, I expect doctrine to remove the exportDestination from the database since I have orphanremoval=true.
What I think the problem is, is that the removeExportDestination() method never gets called during deserialization what should set the relation to null on the inversed side. If that doesn't happen it will not delete the entity since it's not become an orphan yet.
Is there a way that JMS will use the add/remove methods for ArrayCollections during deserialization?
I've also tried to use merge() instead of persist() but did not make any difference
You are right about that "the removeExportDestination() method never gets called during deserialization".
You could define the accessor property to do what you want:
exportDestinations:
groups: ['details', 'put', 'patch', 'post']
expose: true
type: 'ArrayCollection<AppBundle\Entity\ExportDestination>'
accessor:
getter: "getExportDestinations"
setter: "setExportDestinations"
and in ExportJob entity:
public function getExportDestinations()
{
return $this->exportDestinations;
}
public function setExportDestinations($exportDestinations)
{
// first detach existing related entities.
foreach ($this->exportDestinations as $exportDestination) {
$exportDestination->setExportJob(null);
}
$this->exportDestinations = $exportDestinations;
foreach ($exportDestinations as $exportDestination) {
$exportDestination->setExportJob($this);
}
}
so that during deserialization "setExportDestinations" is called and it takes care of the relation removal.
Having said that, I am not sure if you should use orphanremoval=true on the relation, as
orphanRemoval=true, even if you will remove given ExportDestination from one ExportJob, and then attach to another ExportJob, this ExportDestination will be deleted during persist, because the reference has been deleted.
I would suggest to remove it and find another way to delete "orphan" ExportDestination entities.
I have built a custom entity that works well. One of my fields is a taxonomy but I can not retrieve the name of the term in the buildRow(EntityInterface $entity) function which displays my records.
For a simple string field I do: $row['foo'] = $entity->foo->value;
How to do a taxonomy term that is an entity_reference: $row['bar'] = $entity->BAR_TERM_NAME;
Thank you for your help.
To work as requested you need 3 things:
Implements an entity_reference field in your custom Entity.
Add a getter methode for you field.
Retrieve your field in your custom ListBuilder -> buildRow().
Check the Drupal 8 documentation about FieldTypes, FieldWidgets and FieldFormatters.
Implements an entity_reference field
Your field foo in your Entity should be generated using the entity_reference field type.
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
// Some code ...
$fields['foo'] = BaseFieldDefinition::create('entity_reference')
->setLabel($this->t('Foo field'))
->setDescription($this->t('The Foo field.'))
->setSetting('target_type', 'taxonomy_term')
->setSetting('handler', 'default')
->setSetting('handler_settings', ['target_bundles' => ['vocabulary_id' => 'vocabulary_id']])
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'vocabulary_id',
'weight' => 0,
])
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => 40,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayConfigurable('view', TRUE);
// Some code ...
}
You should then replace the 3 vocabulary_id by the vocabulary that you wanna links.
Add a getter
In the same Class as your baseFieldDefinitions.
// Some code ...
public function getFoo() {
return $this->get('foo')->value;
}
// Some code ...
Retrieve the field
In your ListBuilder Class.
public function buildRow(EntityInterface $entity) {
// Some code ...
$row['foo'] = $entity->getFoo();
// Some code ...
}
Hopes it will help you !
Following Doctrine 2's enum defining a type guide, I have the following class:
class EnumStatusType extends EnumType
{
protected $name = 'enumStatusType';
protected $values = [
'active',
];
}
Now, using vendor/bin/doctrine-module migrations:diff, or vendor/bin/doctrine-module orm:schema-tool:update or whichever you prefer, it successfully creates the column with the enum:
status ENUM(\'active\') COMMENT \'(DC2Type:enumStatusType)\' NOT NULL
Now, I wanted to add a second value, inactive. But after running orm:validate-schema, orm:schema-tool:update migrations:diff, none of them notices there is a new value.
How can I make it so that it detects this type of changes, so that a new migration can be made with migrations:diff?
PS: I'm using ZF2, with the DoctrineORMModule. Not that it should matter though.
You can try adding the enum values list in each field comment option, using the postGenerateSchema event:
class EnumListener
{
public function postGenerateSchema(\Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs $eventArgs)
{
$columns = [];
foreach ($eventArgs->getSchema()->getTables() as $table) {
foreach ($table->getColumns() as $column) {
if ($column->getType() instanceof EnumType) {
$columns[] = $column;
}
}
}
/** #var \Doctrine\DBAL\Schema\Column $column */
foreach ($columns as $column) {
$column->setComment(trim(sprintf('%s (%s)', $column->getComment(), implode(',', $column->getType()->getEnum()::toArray()))));
}
}
}
Works for the orm:schema-tool:update command, I suppose it's the same for migrations:diff
I have an entity, call it Stones and Stones has a ManyToMany relationship with Attributes.
So I query the entity to get the Stones and then I hydrate this to convert it into an array.
$result = $this->stoneRepository->find($stone_id);
if ( ! $result )
{
return false;
}
$resultArray = $this->doctrineHydrator->extract($result);
This works fine for the Stone entity however I noticed that the join (Attributes) remain as objects.
array (size=12)
'id' => int 1
'name' => string 'Agate' (length=5)
'title' => string 'Title' (length=5)
'attribute' =>
array (size=5)
0 =>
object(Stone\Entity\StAttribute)[1935]
private 'id' => int 2
private 'name' => string 'Hay fevor' (length=9)
private 'state' => boolean true
private 'created' => null
private 'modified' => null
1 =>
object(Stone\Entity\StAttribute)[1936]
private 'id' => int 15
private 'name' => string 'Libra' (length=5)
private 'state' => boolean true
private 'created' => null
private 'modified' => null
2 =>
etc.
What is the process to hydrate the Attribute objects?
Hydration is populating an object (entity) using an array which is opposite of the extraction.
Since you want the resultset in array format, you should prevent unnecessary hydration and extraction process which already occurs in the ORM level under the hood.
Try to use Query Builder Api instead of built-in find() method of the entity repository. This is not a single-line but really straightforward and faster solution, it should work:
$qb = $this->stoneRepository->createQueryBuilder('S');
$query = $qb->addSelect('A')
->leftJoin('S.attribute', 'A')
->where('S.id = :sid')
->setParameter('sid', (int) $stone_id)
->getQuery();
$resultArray = $query->getOneOrNullResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
This way, you will also prevent running additional SQL queries against database to fetch associated entities. (StAttribute in your case)
I thought I would follow up on this to show how this can be resolved using a CustomStrategy.
By far the easiest and fastest method was suggested by foozy. What I like about the solution is that when I use hydration in ApiGility for instance I can build custom queries which will produce the desired result in a very few lines of code.
The other solution I was working on was to add a custom strategy:
<?php
namespace Api\V1\Rest\Stone;
use DoctrineModule\Stdlib\Hydrator\Strategy\AbstractCollectionStrategy;
use Zend\Stdlib\Hydrator\Strategy\StrategyInterface;
class CustomStrategy extends AbstractCollectionStrategy
{
public function __construct($hydrator)
{
$this->hydrator = $hydrator;
}
/**
* #param mixed $values
* #return array|mixed
*/
public function extract($values)
{
$returnArray = [];
foreach ($values AS $value)
{
$returnArray[] = $this->hydrator->extract($value);
}
return $returnArray;
}
/**
* #param mixed $values
* #return mixed
*/
public function hydrate($values)
{
$returnArray = [];
foreach ($values AS $value )
{
$returnArray[] = $this->hydrator->hydrate($value);
}
return $returnArray;
}
}
Then from the service side I add various strategies to the hydrator like so:
$result = $this->stoneRepository->find($stone_id);
$this->doctrineHydrator->addStrategy("product", new CustomStrategy( $this->doctrineHydrator ) );
$this->doctrineHydrator->addStrategy("attribute", new CustomStrategy( $this->doctrineHydrator ) );
$this->doctrineHydrator->addStrategy("image", new CustomStrategy( $this->doctrineHydrator ) );
$this->doctrineHydrator->addStrategy("related", new CustomStrategy( $this->doctrineHydrator ) );
$resultArray = $this->doctrineHydrator->extract($result);
After which I created a custom entity:
<?php
namespace Api\V1\Rest\Stone;
class StoneEntity
{
public $id;
public $name;
public $description;
public $code;
public $attribute;
public $product;
public $image;
public function getArrayCopy()
{
return array(
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'code' => $this->code,
'attribute' => $this->attribute,
'product' => $this->product,
'image' => $this->image
);
}
public function exchangeArray(array $array)
{
$this->id = $array['id'];
$this->name = $array['name'];
$this->description = $array['description'];
$this->code = $array['code'];
$this->attribute = $array['attribute'];
$this->product = $array['product'];
$this->image = $array['image'];
}
}
And the final part is to exchange the returned data with the custom entity:
$entity = new StoneEntity();
$entity->exchangeArray($resultArray);
And finally to return the result:
return $entity;
To be honest, the above is just too long winded and my final solution as per the suggestion by foozy was this:
public function fetchOne($stone_id)
{
$qb = $this->stoneRepository->createQueryBuilder('S');
$query = $qb->addSelect('A','P','I','C')
->leftJoin('S.attribute', 'A')
->innerJoin('A.category', 'C')
->innerJoin('S.product' , 'P')
->innerJoin('S.image' , 'I')
->where('S.id = :sid')
->setParameter('sid', (int) $stone_id)
->getQuery();
$resultArray = $query->getOneOrNullResult(\Doctrine\ORM\Query::HYDRATE_ARRAY);
if ( ! $resultArray )
{
return false;
}
return $resultArray;
}
i am writing webservice in symfony2 but i facing some problem regarding the output ,as it is giving blank output.
class DefaultController extends Controller {
/**
*
* #Route("/webservices/activity/{id}", name="user_json_activity")
* #Method("get")
*/
public function activityAction($id) {
$em = $this->getDoctrine()->getEntityManager();
$list = $em->getRepository('FitugowebserviceBundle:activity')->findOneById($id);
$r_array = $this->routes2Array($list);
$r = array('activity' => $r_array);
return new Response(json_encode($r));
}
private function routes2Array($routes) {
$points_array = array();
foreach ($routes as $route) {
$r_array = array('activity' => $route->getActivity(),
'icon' => $route->getIcon());
$points_array[] = $r_array;
}
return $points_array;
}
}
When i try to fetch data for id=1 http://domain.org/fitugo/web/app_dev.php/webservices/activity/1 it is giving output as follows
{"activity":[]}
It look very strange that you want get array with findOneById method. The first thing I suggest to add a check that the entity founded by id exist. Then look that findOneById returns and check your controller logic.