I'm trying to extend Sylius\Component\Core\Model\Taxon by adding new data fields. The same procedure did work on another model outside of Sylius Core. When running doctrine:migrations:diff, the error message is "The table with name 'sylius_dev.sylius_taxon' already exists."
The response for php bin/console debug:container --parameter=sylius.model.taxon.class does not change at all.
This is my new class in /src/AppBundle/Entity/FooTaxon.php:
<?php
namespace AppBundle\Entity;
use Sylius\Component\Core\Model\Taxon as BaseTaxon;
class FooTaxon extends BaseTaxon
{
/**
* #var string
*/
private $field_one;
/**
* #return string
*/
public function getFieldOne(): string
{
return $this->field_one;
}
/**
* #param string $new_value
*/
public function setFieldOne(string $new_value): void
{
$this->field_one = $new_value;
}
/**
* #var int
*/
private $field_two;
/**
* #return int
*/
public function getFieldTwo(): int
{
return $this->field_two;
}
/**
* #param int $new_value
*/
public function setFieldTwo(int $new_value): void
{
$this->field_two = $new_value;
}
}
This is my /src/AppBundle/Resources/config/doctrine/FooTaxon.orm.yml:
AppBundle\Entity\FooTaxon:
type: entity
table: sylius_taxon
fields:
field_one:
type: string
nullable: false
field_two:
type: integer
nullable: false
And here is the new entry in /app/config/config.yml:
sylius_core:
resources:
product_taxon:
classes:
model: AppBundle\Entity\FooTaxon
Any help would be appreciated since I'm new to both Symfony and Sylius.
You should use this instead of sylius_core node:
sylius_taxonomy:
resources:
taxon:
classes:
model: AppBundle\Entity\FooTaxon
And better use upperCase in entity property names instead of snake_case.
Related
I have the structure like below.
----------------
MESSAGE
----------------
id
subject
body
----------------
----------------
USER
----------------
id
name
category
region
----------------
----------------
RECIPIENT
----------------
user_id
message_id
is_read
read_at
----------------
So Message 1:n Recipient m:1 User.
Recipient is not an #ApiResource.
A Backoffice user will "write" a message and choose the audience by a set of specific criteria (user region, user category, user tags...).
To POST the message i'm using a Dto
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public DateTimeInterface $publishDate;
/**
* #var DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
*/
public array $criteria = [];
}
The $criteria field is used to choose the audience of that message and is skipped by the DataTransformer as it is not a mapped field, a property of Message Entity that is returned by the transformer.
class MessageInputDataTransformer implements \ApiPlatform\Core\DataTransformer\DataTransformerInterface
{
/**
* #var MessageInputDto $object
* #inheritDoc
*/
public function transform($object, string $to, array $context = [])
{
$message = new Message($object->subject, $object->body);
$message->setIsPublished($object->isPublished);
$message->setPublishDate($object->publishDate);
$message->setExpiryDate($object->expiryDate);
$message->setCategory($object->category);
return $message;
}
/**
* #inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
// in the case of an input, the value given here is an array (the JSON decoded).
// if it's a book we transformed the data already
if ($data instanceof Message) {
return false;
}
return Message::class === $to && null !== ($context['input']['class'] ?? null);
}
}
As side effect, will be performed a bulk insert in the join table (Recipient) that keeps the m:n relations between Message and User.
My problem is how/where to perform this bulk insert and how pass the $criteria to the service that will manage it.
The only solution that i've found now (and it's working but i don't think is a good practice) is to put the bulk insert procedure in the POST_WRITE event of the Message, get the Request object and process the $criteria contained there.
class MessageSubscriber implements EventSubscriberInterface
{
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => [
['handleCriteria', EventPriorities::POST_WRITE]
],
];
}
public function handleCriteria(ViewEvent $event)
{
/** #var Message $message */
$message = $event->getControllerResult();
$method = $event->getRequest()->getMethod();
$e = $event->getRequest();
$collectionOperation = $e->get('_api_collection_operation_name');
if (!$message instanceof Message ||
$method !== Request::METHOD_POST ||
$collectionOperation !== 'post') {
return;
}
$content = json_decode($event->getRequest()->getContent(), true);
if(array_key_exists('audienceCriteria', $content)){
$criteria = Criteria::createFromArray($content['audienceCriteria']);
// Todo: Create the audience
}
}
}
So the idea is that, when the Message is persisted, the system must generate the "relations" public.
This is why i think that the post write event could be a good choice, but as i said i'm not sure this could be a good practice.
Any idea? Thanks.
As the docs on DTO's state: "in most cases the DTO pattern should be implemented using an API Resource class representing the public data model exposed through the API and a custom data provider. In such cases, the class marked with #ApiResource will act as a DTO."
IOW specifying an Input or an Output Data Representation and a DataTransformer is the exception. It does not work if the DTO holds more data then the entity or if the dto's are not one to one with the entities (for example with a report that does a group by).
Here is your DTO class as a resource:
namespace App\DTO;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
use App\Entity\Message;
/**
* Class defining Message data transfer
*
* #ApiResource(
* denormalizationContext= {"groups" = {"msg_message:write"}},
* itemOperations={
* },
* collectionOperations={
* "post"={
* "path"="/messages",
* "openapi_context" = {
* "summary" = "Creates a Message",
* "description" = "Creates a Message"
* }
* }
* },
* output=Message::class
* )
*/
class MessageInputDto
{
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $subject;
/**
* #var string
*
* #Groups({"msg_message:write"})
*/
public string $body;
/**
* #var bool
*
* #Groups({"msg_message:write"})
*/
public bool $isPublished;
/**
* #var \DateTimeInterface
*
* #Groups({"msg_message:write"})
*/
public \DateTimeInterface $publishDate;
/**
* #var \DateTimeInterface|null
*
* #Groups({"msg_message:write"})
*/
public ?\DateTimeInterface $expiryDate = null;
/**
* #var MessageCategory|null
*
* #Groups({"msg_message:write"})
*/
public ?MessageCategory $category = null;
/**
* #var array
* #Groups({"msg_message:write"})
*/
public array $criteria = [];
}
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
If MessageInputDto is in /src/DTO make it like:
api_platform:
mapping:
paths:
- '%kernel.project_dir%/src/Entity'
- '%kernel.project_dir%/src/DTO'
The post operation may have the same path as dhe default post operation on your Message resource. Remove that by explicitly defining collectionOperations for your Message resource without "post".
The post operation of MessageInputDto will deserialize the MessageInputDto. Your DataTransformer will not act on it so that it will arrive as is to the DataPersister:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\DTO\MessageInputDto;
use App\Entity\Message;
use Doctrine\Persistence\ManagerRegistry;
use App\DataTransformer\MessageInputDataTransformer;
use ApiPlatform\Core\Exception\InvalidArgumentException;
class MessageDataPersister implements ContextAwareDataPersisterInterface
{
private $dataPersister;
private $entityManager;
private $dataTransformer;
public function __construct(ContextAwareDataPersisterInterface $dataPersister, ManagerRegistry $managerRegistry, MessageInputDataTransformer $dataTransformer)
{
$this->dataPersister = $dataPersister;
$this->entityManager = $managerRegistry->getManagerForClass(Message::class);
$this->dataTransformer = $dataTransformer;
}
public function supports($data, array $context = []): bool
{
$transformationContext = ['input' => ['class' => Message::class]];
return get_class($data) == MessageInputDto::class
&& $this->dataTransformer->supportsTransformation($data, Message::class, $transformationContext)
&& null !== $this->entityManager;
}
public function persist($data, array $context = [])
{
$message = $this->dataTransformer->transform($data, Message::class);
// dataPersister will flush the entityManager but we do not want incomplete data inserted
$this->entityManager->beginTransaction();
$commit = true;
$result = $this->dataPersister->persist($message, []);
if(!empty($data->criteria)){
$criteria = Criteria::createFromArray($data->criteria);
try {
// Todo: Create the audience, preferably with a single INSERT query SELECTing FROM user_table WHERE meeting the criteria
// (Or maybe better postpone until message is really sent, user region, category, tags may change over time)
} catch (\Exception $e) {
$commit = false;
$this->entityManager->rollback();
}
}
if ($commit) {
$this->entityManager->commit();
}
return $result;
}
public function remove($data, array $context = [])
{
throw new InvalidArgumentException('Operation not supported: delete');
}
}
(Maybe it should have been called MessageInputDtoDataPersister - depending on how you look at it)
Even with service autowiring and autoconfiguration enabled, you must still configure it to get the right dataPersister to delegate to:
# api/config/services.yaml
services:
# ...
'App\DataPersister\MessageDataPersister':
arguments:
$dataPersister: '#api_platform.doctrine.orm.data_persister'
This way you do not need MessageSubscriber.
Be aware that all the other phases inbetween deserialization and data persist (validation, security post denormalize) work on the MessageInputDto.
One solution when you have to generate multiple custom entities is to use data persisters: https://api-platform.com/docs/core/data-persisters/
There you have 2 options:
Decorate the doctrine persister - meaning the message will still be saved by Doctrine, but you can do something before or afterwards.
Implement a custom persister - saving both message and other related entities that you like. Or doing something completely custom, without calling Doctrine at all.
I tried map data from a given array to an object with the jms serializer (in a unit test) to test a doctrine entity:
Given is a simple entity class:
/**
* CashPosition
*/
class CashPosition
{
/**
* #var integer
*/
protected $cashPositionId;
/**
* #var \DateTime
*/
protected $date;
/**
* #var float
*/
protected $value;
/**
* Get cashPositionId
*
* #return integer
*/
public function getCashPositionId()
{
return $this->cashPositionId;
}
/**
* Set date
*
* #param \DateTime $date
*
* #return $this
*/
public function setDate($date)
{
$this->date = $date;
return $this;
}
/**
* Get date
*
* #return \DateTime
*/
public function getDate()
{
return $this->date;
}
/**
* Set value
*
* #param string $value
*
* #return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Get value
*
* #return float
*/
public function getValue()
{
return $this->value;
}
}
I defined the serialization under Resources\config\serializer\Entity.CashPosition.yml
MyBundle\Entity\CashPosition:
exclusion_policy: ALL
access_type: public_method
properties:
cashPositionId:
exclude: false
expose: true
type: integer
access_type: property
date:
exclude: false
expose: true
type: DateTime<'Y-m-d'>
value:
exclude: false
expose: true
type: float
And tried to cover this with a serialization test:
public function testSerialization()
{
$data = [
'cashPositionId' => 1,
'date' => date('Y-m-d'),
'value' => 1.0,
];
/* #var $serializer Serializer */
$serializer = $this->container->get('serializer');
$cashPosition = $serializer->fromArray($data, CashPosition::class);
$this->assertInstanceOf(CashPosition::class, $cashPosition);
$this->assertEquals($data, $serializer->toArray($cashPosition));
}
But the test fails since the fromArray method does not set the cashPositionId. I tried some different configurations with the access type but had no luck. I'm not sure what's the problem here.
I'm using the following version of jms serializer:
jms/metadata 1.6.0 Class/method/property metadata management in PHP
jms/parser-lib 1.0.0 A library for easily creating recursive-descent parsers.
jms/serializer 1.6.2 Library for (de-)serializing data of any complexity; supports XML, JSON, and YAML.
jms/serializer-bundle 1.4.0 Allows you to easily serialize, and deserialize data of any complexity
Hello i think you miss the serialized_name property for cashPositionId, by default jms will translate properties from camel case to snake case.
JMS Doc
I have several classes using a Taggable trait to set up a tag system common to several doctrine entities (Project, Note, ...).
The relationship between these entities and these tags is a ManyToMany relationship that I can not make multi-directional.
My problem: When I delete a Project entity, it is removed from the project table, but the relationships in the project_tag table between this project and the tags are not deleted. Then, if I create a new Project entity, an exception is thrown.
An exception exists while executing 'INSERT INTO project_tag (project_id, tag_id) VALUES (?,?)' With params [2, 4]:
SQLSTATE [23000]: Integrity constraint violation: 19 UNIQUE constraint failed: project_tag.project_id, project_tag.tag_id
Entities :
Tag
/**
* Tag
*
* #ORM\Table(name="tag")
* #ORM\Entity(repositoryClass="AppBundle\Repository\TagRepository")
*/
class Tag
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* #ORM\Column(name="last_use_at", type="datetime", nullable=false)
* #var \DateTime
*/
private $lastUseAt;
public function __construct()
{
$this->lastUseAt = new \DateTime();
}
public function __toString()
{
return $this->name;
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Tag
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #return \DateTime
*/
public function getLastUseAt(): \DateTime
{
return $this->lastUseAt;
}
/**
* #param \DateTime $lastUseAt
*/
public function setLastUseAt(\DateTime $lastUseAt)
{
$this->lastUseAt = $lastUseAt;
}
}
Taggable
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Tag", cascade={"persist"})
*/
protected $tags;
/**
* Add tag
*
* #param Tag $tag
*
* #return $this
*/
public function addTag(Tag $tag)
{
$tag->setLastUseAt(new \DateTime());
$this->tags[] = $tag;
return $this;
}
/**
* Remove tag
*
* #param Tag $tag
*/
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
Project
/**
* Project
*
* #ORM\Table(name="project")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProjectRepository")
*/
class Project
{
use Taggable;
}
Note
class Note
{
use Taggable;
}
Is this the only solution or is my annotation incomplete / incorrect?
I tried with JoinColumns, JoinTable and onDelete = "cascade" but nothing works.
In the meantime, I dodged the problem with this instruction placed before the suppresion.
$project->getTags()->clear();
Full code of the action in the controller :
/**
* #Route("/project/{id}/delete", name="project_delete")
*/
public function deleteAction($id) {
$em = $this->getDoctrine()->getManager();
$project = $em->getRepository('AppBundle:Project')->find($id);
if(!$project) {
return $this->redirectToRoute('index');
}
$project->getTags()->clear();
$em->remove($project);
$em->flush();
return $this->redirectToRoute('index');
}
I think I found a better solution: you can set the PRAGMA within Doctrine configuration. Like:
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_sqlite'
#server_version: '5.7'
#charset: utf8mb4
#default_table_options:
#charset: utf8mb4
#collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
options:
'PRAGMA foreign_keys': 'ON'
I just tried it on my Symfony 4 application, re-created the database and tested using DB Browser for SQLite and it works as I expected.
Hope this helps
I managed to fix the problem. Here's my solution working for SQLite conections.
Create an eventListener listening on the kernel.request event :
namespace AppBundle\EventListener;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
/**
* #var Registry
*/
private $doctrine;
public function __construct(Registry $doctrine)
{
$this->doctrine = $doctrine;
}
public function onKernelRequest(GetResponseEvent $event)
{
$this->doctrine->getConnection()->exec('PRAGMA foreign_keys = ON');
}
}
Service declaration
app.event_listener.request_listener:
class: AppBundle\EventListener\RequestListener
arguments:
- '#doctrine'
tags:
- { name: kernel.event_listener, event: kernel.request }
I think the problem is that you have your trait Taggable set as the owning side of the ManyToMany relationship but your are deleting the inverse side and expecting something to happen as a result. Doctrine will only check the owning side of the relationship in order to persist any changes. See here for docs on this.
You can solve by making the Taggable the inverse side of each of your relationships, or by manually telling doctrine to delete the owning side.
The first solution will probably not work for you since you won't (easily) specify multiple inverse sides. (Are you sure a trait is the right way to go for this??)
The second solution is easy. In your entities like Project for your deleteTag($tag) function, call a delete function on the owning side (e.g., deleteProject($project). You will have to create if one does not exist.
class Project
{
use Taggable;
public function deleteTag($tag)
{
$this->tags->removeElement($tag);
// persist on the owning side
$tag->deleteProject($this);
}
}
EDIT:
After seeing full code, it looks like you are deleting correctly. Now you need to tell doctrine to carry that through. See this post for full details, but basically you can change your trait to this:
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\Tag",
* cascade={"persist"},
* onDelete="CASCADE"
* )
*/
protected $tags;
// ...
}
I have this entity definition:
class Operator
{
...
/**
* #var array
* #ORM\Column(type="text", nullable=true)
*/
private $prefix;
/**
* #param $prefix
* #return $this
*/
public function addPrefix($prefix)
{
if (!in_array($prefix, $this->prefix, true)) {
$this->prefix[] = $prefix;
}
return $this;
}
/**
* #param array $prefixes
* #return $this
*/
public function setPrefix(array $prefixes)
{
$this->prefix = array();
foreach($prefixes as $prefix) {
$this->addPrefix($prefix);
}
return $this;
}
/**
* #return array The prefixes
*/
public function getPrefix()
{
$prefix = is_array($this->prefix) ? $this->prefix : ['04XX'];
return array_unique($prefix);
}
...
}
I am using EasyAdminBundle for manage this entity in the backend so here is the config for it:
easy_admin:
entities:
Operator:
class: PlatformAdminBundle\Entity\Operator
...
form:
fields:
...
- { property: 'prefix', label: 'prefix' }
Any time I try to create a new Operator I run into this error:
ContextErrorException: Notice: Array to string conversion
I can't find where is the problem since I am using the same on a User entity that inherit from BaseUser (from FOSUser) and it works. This is how it looks like for User entity and should be the same for Operator:
What I am missing? Can any give me some advice? I am stuck!
Orm prefix column should be array type.
/**
* #var array
* #ORM\Column(type="array", nullable=true)
*/
private $prefix;
And run
php app/console doctrine:schema:update --force
I'm using couchDb in symfony 2.7.2.
I have several doubts.
Now I installed this Bundle
And I create one entity for testing
<?php
namespace foo\GarageBundle\Document;
use Doctrine\ODM\CouchDB\Mapping\Annotations as CouchDB;
/**
* #CouchDB\Document
*/
class Utente
{
/** #CouchDB\Id */
private $id;
/** #CouchDB\Field(type="string") */
private $nome;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set nome
*
* #param string $nome
* #return Utente
*/
public function setNome($nome)
{
$this->nome = $nome;
return $this;
}
/**
* Get nome
*
* #return string
*/
public function getNome()
{
return $this->nome;
}
}
In my controller I added this Code
$dm = $this->container->get('doctrine_couchdb.client.default_connection');
$doc = $this->container->get('doctrine_couchdb.odm.default_document_manager');
try{
$dm->createDatabase($dm->getDatabase());
}catch(\Exception $e){
$msg = $e->getMessage();
}
$user = new Utente();
$user->setNome('foo');
$doc->persist($user);
$doc->flush();
my config.yml is
doctrine_couch_db:
client:
default_connection: default
connections:
default:
dbname: symfony2
odm:
default_document_manager: default
document_managers:
default:
auto_mapping: true
With controller I created Database but I can't insert the new Document, I got this error
The class 'foo\GarageBundle\Document\Utente' was not found in the chain configured namespaces
And I don't understand why it is useful to use a bundle as what I am using ( I know it could be a stupid question ), and why I have to use * #CouchDB\Document instead of #Document inside my entity ?
Seems a problem related the namespace of the entity class.
The automapping is registering the CouchDocument subnamespace of
your bundle, not Document (which is auto-mapped by
DoctrineMongoDBBundle)
So use a different namespace for the User class and the other Counch you use, as follow:
namespace foo\GarageBundle\CouchDocument;
In particular:
<?php
namespace foo\GarageBundle\CouchDocument;
use Doctrine\ODM\CouchDB\Mapping\Annotations as CouchDB;
/**
* #CouchDB\Document
*/
class Utente
{
Hope this help
See this discussion on github.
/**
* #CouchDB\Document
* #CouchDB\Index
*/
class Utente
{