I'm quite new to ZF2. I have a bunch of doctrine entities from my project in ZF1, one being a User entity.
I'm trying to extend \ZfcUserDoctrineORM\Entity\User so that I can inclue my old relationships - but with no great success.
If I try and run the schema generator ./doctrine-module orm:schema-tool:create I get an error saying the table name user already exists.
So far I've tried extending the class and setting my class as the UserEntityClass
return array(
'zfcuser' => array(
'UserEntityClass' => '\Application\Entity\User',
),
);
There doen't seem to be any documentation for the module as yet.
So the problem was that the default ZfcUserDoctrineORM entities were still in play. To solve this you can set EnableDefaultEntities to false like so :
return array(
'zfcuser' => array(
'UserEntityClass' => '\Acme\Entity\User',
'EnableDefaultEntities' => false
),
);
I'm going from memory here, as I played around with ZfcUser[DoctrineORM] some time back.
You don't want to extend anything. Instead, just write your own User entity and make it implement ZfcUser\Entity\UserInterface.
Then make sure you set your configuration correctly (to use your own implementation of UserInterface, instead of the default), and you should be good to go.
Though the OP's answer is correct, it's not complete. Below what I've done and why.
Create own User Entity. AbstractEntity is one used for all my Entities, contains just the ID property, getters/setter and some global functionality for Entity debugging.
User.php
use Doctrine\ORM\Mapping as ORM;
use Keet\Mvc\Entity\AbstractEntity;
use ZfcUser\Entity\UserInterface;
/**
* Entity Class representing a post of our User module.
*
* #ORM\Entity
* #ORM\Table(name="users")
* #ORM\Entity(repositoryClass="Keet\User\Repository\UserRepository")
* #ORM\HasLifecycleCallbacks
*/
class User extends AbstractEntity implements UserInterface
{
/**
* #var string
* #ORM\Column(name="username", type="string", length=255, unique=true, nullable=false)
*/
protected $username;
/**
* #var string
* #ORM\Column(name="email", type="string", length=255, unique=true, nullable=false)
*/
protected $email;
/**
* #var string
* #ORM\Column(name="display_name", type="string", length=255, nullable=false)
*/
protected $displayName;
/**
* #var string
* #ORM\Column(name="`password`", type="string", length=255, nullable=false)
*/
protected $password;
/**
* #var int
* #ORM\Column(name="state", type="integer", length=3, nullable=false)
*/
protected $state = 1;
// Getters/Setters
}
zfcuser.config.php
Contains the same config as OP's answer:
'zfcuser' => [
//Some other config
'userEntityClass' => User::class,
'EnableDefaultEntities' => false,
],
module.config.php
IMPORTANT: Overwrite the default doctrine entity config of zfcuser!
'doctrine' => [
'orm_autoload_annotations' => true,
'driver' => [
__NAMESPACE__ . '_driver' => [
'class' => 'Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver',
'cache' => 'array',
'paths' => [
__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src'
. DIRECTORY_SEPARATOR . 'Entity',
]
],
'orm_default' => [
'drivers' => [
__NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_driver',
'ZfcUser\Entity' => __NAMESPACE__ . '_driver'
],
],
'zfcuser_entity' => [ // Section overwrites the default config for location of Annotation. Original found
// in vendor ZfcUserDoctrineModule /config/xml/zfcuser/ZfcUser.Entity.User.dcm.xml
'class' => 'Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver',
'paths' =>
__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src'
. DIRECTORY_SEPARATOR . 'Entity',
],
],
],
In the above you'd normally just have the 'driver' and 'orm_default' configs. ZfcUser module contains a the 'zfcuser_entity' config, but points it to the driver within it's own module. To not have a floating reference to a different driver, overwrite it and point it towards your own defined driver, defined by __NAMESPACE__ . '_driver'. Also, in my example I use the AnnotationDriver for reading annotation, whereas the ZfcUser module uses the XmlDriver. ZfcUser module config below
This is the original config, overwritten in the above example
'zfcuser_entity' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\XmlDriver',
'paths' => __DIR__ . '/xml/zfcuser'
),
This config allows you to fully use your own Entity, but still use the ZfcUser module.
In my case I can use my own AbstractActionController with my self-written CRUD actions, which expect a child of AbstractEntity. This saves a lot of writing/thinking ;)
Related
I am using Doctrine2 within a Laminas API project to create a table inheritance like this:
Content:
/**
* #ORM\Entity()
* #ORM\Table(name="content")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({"content" = "Content", "image"="Image", "video"="Video"})
*/
class Content {
/**
* #ORM\Id
* #ORM\Column(name="id")
* #ORM\GeneratedValue
*/
protected $id;
/**
* #ORM\Column(name="name", type="string")
*/
protected $name;
[... some other fields and getters and setters]
}
Image:
/**
* #ORM\Entity()
* #ORM\Table(name="image")
*/
class Image extends Content {
/**
* #ORM\Column(name="filesize", type="integer")
*/
protected $filesize;
[... some other fields and getters and setters]
}
I want to create new Images and Videos by posting data to the /content endpoint in my app. Based on the type of the file I create either a new Video() or a new Image(). But when I call new Image() in my Mapper, I get the following error:
Unable to determine entity identifier for object of type
"App\V1\Entity\Image"; no fields matching "id"
I tried to duplicate the fields and Getters and Setters of content including the id to the image object with the same result. I also created a new endpoint /images and tried posting image data to that endpoint, also with the same result.
I use the same setup in the Laminas-MVC variant of this app, which I now want to migrate to Laminas-API. In the MVC app, this works.
How can I create new images?
[UPDATE]
Here is the API tools hal config:
'api-tools-hal' => [
'metadata_map' => [
DNSad\V1\Entity\Content::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.content',
'route_identifier_name' => 'content_id',
'hydrator' => \Doctrine\Laminas\Hydrator\DoctrineObject::class,
],
DNSad\V1\Collection\Content::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.content',
'route_identifier_name' => 'content_id',
'is_collection' => true,
],
DNSad\V1\Entity\Image::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.image',
'route_identifier_name' => 'image_id',
'hydrator' => \Doctrine\Laminas\Hydrator\DoctrineObject::class,
],
DNSad\V1\Collection\Images::class => [
'max_depth' => 1,
'entity_identifier_name' => 'id',
'route_name' => 'dnsad.rest.image',
'route_identifier_name' => 'image_id',
'is_collection' => true,
],
],
I tried different hydrators and configs for the image object, but the problem still persists. Is there a special configuration necessary to set up the table inheritance here ?
[EDIT]
Removing 'entity_identifier_name' => 'id', from the image entity config solves the issue
I'm trying to soft delete a complete Customer. A Customer extends User. Customer also has associated InvoiceAddress[] entities.
It however, does not work. If the Customer has #Gedmo\SoftDeleteable, it fails on the Foreign Key association with User. If I also make the User entity soft delete-able, then it fails on the association between Customer and InvoiceAddress.
If I make the relation between Customer and InvoiceAddress to cascade={"persist", "remove"} (added remove), then it hard deletes all entities related to the Customer.
I figure it might be something in the configuration, though having read multiple questions and (of course) the docs of the SoftDeleteable extension itself, I haven't figured out what/where I'm doing something wrong.
Below is my setup, I've removed stuff from the code unrelated to the question.
Customer.php
namespace Customer\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Gedmo\SoftDeleteable\SoftDeleteable;
// moar
/**
* #ORM\Table
* #ORM\Entity
*
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class Customer extends User implements SoftDeleteable
{
use GedmoDeletedAtTrait;
/**
* #var ArrayCollection|InvoiceAddress[]
* #ORM\OneToMany(targetEntity="Customer\Entity\InvoiceAddress", mappedBy="customer", cascade={"persist"}, fetch="EAGER")
*/
protected $invoiceAddresses;
// properties, __construct(){}, getters/setters...
}
User.php
namespace User\Entity;
use BjyAuthorize\Provider\Role\ProviderInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Mvc\Entity\AbstractEntity;
use ZfcUser\Entity\UserInterface;
/**
* #ORM\Table
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="discr", type="string")
*/
class User extends AbstractEntity implements UserInterface, ProviderInterface
{
// properties, constructor, getters/setters...
}
GedmoDeletedAtTrait.php
namespace Mvc\Traits;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
trait GedmoDeletedAtTrait
{
use SoftDeleteableEntity;
/**
* Note: overrides Annotation (column name) and type hint, else it's the same as the original
*
* #var \DateTime|null
* #Doctrine\ORM\Mapping\Column(name="deleted_at", type="datetime", nullable=true)
*/
protected $deletedAt;
}
doctrine module config for Customer module
'doctrine' => [
'driver' => [
__NAMESPACE__ . '_driver' => [
'class' => 'Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver',
'cache' => 'array',
'paths' => [
__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src'
. DIRECTORY_SEPARATOR . 'Entity',
]
],
'orm_default' => [
'drivers' => [
__NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_driver'
],
],
],
'eventmanager' => [
'orm_default' => [
'subscribers' => [
SoftDeleteableListener::class,
],
],
],
],
Related question: the docs also mention "filters". How to implement them and use them throughout a module with the setup above?
Found the answer. I was missing a piece of configuration, not (yet) sure as to how it relates to the Listener and the LifecycleCallbacks that need to be executed to soft-delete an Entity, but the complete configuration is as follows:
use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter;
use Gedmo\SoftDeleteable\SoftDeleteableListener;
[ ... ]
'doctrine' => [
'driver' => [
__NAMESPACE__ . '_driver' => [
'class' => 'Doctrine\\ORM\\Mapping\\Driver\\AnnotationDriver',
'cache' => 'array',
'paths' => [
__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src'
. DIRECTORY_SEPARATOR . 'Entity',
]
],
'orm_default' => [
'drivers' => [
__NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_driver'
],
],
],
'eventmanager' => [
'orm_default' => [
'subscribers' => [
SoftDeleteableListener::class,
],
],
],
// THIS IS THE PART THAT WAS MISSING
'configuration' => [
'orm_default' => [
'filters' => [
'soft-deletable' => SoftDeleteableFilter::class,
],
],
],
],
In the above snipped I've marked the missing piece with a comment. However, as that bit just sets a filter to use on an alias, I'm not sure how it relates to the configuration above, which defines a Listener.
If I figure it out later/in the future I might come back and update this answer. In the mean time, maybe someone else might place a comment with the info?
Hi i used composer to create the ZF2 skeleton app.
Installed doctrine-module, doctrine-orm-module etc. composer file below:
{
"name": "zendframework/skeleton-application",
"description": "Skeleton Application for ZF2",
"license": "BSD-3-Clause",
"keywords": [
"framework",
"zf2"
],
"homepage": "http://framework.zend.com/",
"require": {
"php": ">=5.3.3",
"zendframework/zendframework": "2.2.*",
"doctrine/doctrine-orm-module": "0.*",
"doctrine/data-fixtures": "dev-master",
"zendframework/zend-developer-tools": "dev-master",
"doctrine/migrations": "dev-master",
"bjyoungblood/bjy-profiler": "dev-master",
"zendframework/zftool": "dev-master"
}
}
Added doctrine config to the module as follows:
'doctrine' => array(
'driver' => array(
__NAMESPACE__.'_entities' => array(
'class' =>'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
//'cache' => 'array',
'paths' => array(
__DIR__ . '/../src/'. __NAMESPACE__ .'/Entity',
)
),
'orm_default' => array(
'drivers' => array(
__NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_entities'
)
))),
And i added my entity to:
module/Application/src/Application/Entity/User.php
But when i run:
php zf.php orm:info
i get the following message:
[Exception]
You do not have any mapped Doctrine ORM entities
according to the current configuration. If you have entities or
mapping files you should check your mapping configuration for errors.
If i try
php zf.php orm:schema-tool:create
i get:
No Metadata Classes to process.
How can i get Doctrine to generate my database? What am i doing wrong?
Edit:
Here is the entity code as requested in the comments:
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/** #ORM\Entity */
class User {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/** #ORM\Column(type="string") */
protected $fullName;
}
Small update,
i am actually running this inside of a virtual machine. The folder where the files are is mounted from the host. If i copy files to tmp folder, it works just fine. If i run php zf.php orm:info in the mounted folder i get an error.
Virtualisation used is Paralles 8, host OS is OSX, and guest is Debian 7.
I am working in /media/psf/ mount.
The __NAMESPACE__ directive in your doctrine config must be a problem : your config probably do not use a namespace and so the __NAMESPACE__ will resolve to the main namespace.
You can try a config like this one :
'doctrine' => array(
'driver' => array(
'application_driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'cache' => 'array',
'paths' => array(
'module/Application/src/Application/Entity',
),
),
No need to use __DIR__, you can use the application root as ZendApplication is chrooted (in public/index.php : chdir(dirname(__DIR__)); ).
Finally, you can also specify the table name for your Entity and the getters/setters for mapped columns :
<?php
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User {
/**
* #var int
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #var string
* #ORM\Column(type="string")
*/
protected $fullName;
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return string
*/
public function getFullName()
{
return $this->fullName;
}
/**
* #param string $fullname
*/
public function setFullName($fullname)
{
$this->fullName= $fullname;
}
}
The issue was with outdated parallels tools.
After doing some investigation, i found out there is a ne version of parallels tools available.
Manual update of the tools, and restart of the virtual machine solved the problem.
There was nothing wrong even with the original code, and it all worked fine after the update.
I guess there was a bug in parallels, but i did not find any reference about it online.
Thank you for your help.
I´m struggling with setting up the entities correctly at the moment.
Here´s the situation:
"Country" has one or many "Customers"
I´ve a country table with all countries and I want to save a reference to a country with each customer. Quite simple and often required.
But I´m unable to configure the entities properly. If I don´t define a cascading method in the "Customer" class, I get an exception. If I add a cascading method then the country objects are added as a new record also to the country table but I only want to have a reference to this object in the Customer table.
Customer Class
/**
* #ORM\Entity
* #ORM\Table(name="PS_Customer")
*/
class Customer {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Country",inversedBy="id")
* #ORM\JoinColumn(name="country_id", referencedColumnName="id")
*/
protected $country;
}
Country Class
/**
* #ORM\Entity
* #ORM\Table(name="PS_Country")
*/
class Country {
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #ORM\OneToMany(targetEntity="Customer", mappedBy="country")
*/
protected $id;
/** #ORM\Column(type="string") */
protected $name;
/** #ORM\Column(type="string") */
protected $iso2;
}
If I want to store a Customer object with this definition, I receive the following error:
A new entity was found through the relationship 'Photoshop\Entity\Customer#country' that was not configured to cascade persist operations for entity: Photoshop\Entity\Country#000000004c6f8efb00000000b695d273. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'Photoshop\Entity\Country#__toString()' to get a clue.
ActionController (extracted):
$forms = $this->getServiceLocator()->get('FormElementManager');
$form = $forms->get('Photoshop\Form\CheckoutForm');
$customer = new Customer;
$form->bind($customer);
$order = new Order;
$order->setCustomer($customer);
// Order object is put into a session during checkout process here...
/**
* Commit Order to database
*/
$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
$em->persist($sessionCheckout->order); // Fetch the order object from session
$em->flush();
Checkout Form
class CheckoutForm extends Form implements ObjectManagerAwareInterface {
protected $objectManager;
public function __construct() {
parent::__construct('checkout');
}
public function init() {
$this->setAttribute('action', 'checkout');
$this->setAttribute('method', 'post');
$this->setHydrator(new DoctrineHydrator($this->getObjectManager()));
$this->setInputFilter(new \Photoshop\Form\CheckoutFilter());
$this->add(array(
'name' => 'country',
'type' => 'DoctrineModule\Form\Element\ObjectSelect',
'options' => array(
'label' => 'Country:',
'empty_option' => 'Please choose...',
'object_manager' => $this->getObjectManager(),
'target_class' => 'Photoshop\Entity\Country',
'property' => 'name',
),
));
$this->add(array(
'type' => 'Zend\Form\Element\Select',
'name' => 'gender',
'options' => array(
'label' => 'Title*:',
'empty_option' => 'Please choose...',
'value_options' => array(
'f' => 'Mrs.',
'm' => 'Mr.'
),
)
));
$this->add(array(
'name' => 'firstName',
'attributes' => array(
'type' => 'text',
'id' => 'firstName'
),
'options' => array(
'label' => 'First name*:'
),
));
$this->add(array(
'name' => 'lastName',
'attributes' => array(
'type' => 'text',
'id' => 'lastName'
),
'options' => array(
'label' => 'Last name*:'
),
));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Pay with PayPal or Credit Card now',
'class' => 'btn btn-primary btn-lg btn-block'
)
));
}
public function setObjectManager(ObjectManager $objectManager) {
$this->objectManager = $objectManager;
}
/**
* Get the object manager
*
* #return ObjectManager
*/
public function getObjectManager() {
return $this->objectManager;
}
}
I´m quite sure that it will be simple to solve. But I can´t see the solution at the moment :)
Maybe somebody can give me a hint?!? Would appreciate that...
Thanks,
Michael
// Order object is put into a session during checkout process here... ---- thats the important part
So if I understand, you create order and customer in one request, then transfer it through session to some other request and persist it there. What really happens is that you have object graph like order->customer->country, where first two are new entities, so serializing unserializing does nothing wrong with them, but country is managed entity already existing in DB. By serializing it into session it is detached from entity manager and after unserializing it, it is presented to new entity manager instance, which does not know that it was once managed, so decides to persist is as new one.
Usually you need to merge unserialized entity to current entity manager
$managedOrder = $em->merge($sessionCheckout->order);
and work with $managedOrder. For this to work you might need to set cascade={"merge"} on Customer::country and on Order::customer.
Doctrine has Entities in session documentation page on this subject.
As a New Year's Day hackathon thought I would take Rob Allen's great zend framework 2 beta tutorial and substitute doctrine2 instead of Zend\Db\Table using the ZF2 modules SpiffyDoctrine and SpiffyDoctrineORM.
Everything was going pretty well, got the entity manager going and set up my entity:
<?php
namespace AlbumDoc\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="album")
*/
class Album {
/**
* #ORM\Id
* #ORM\Column(type="integer");
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #ORM\Column(type="string")
*/
public $artist;
/**
* #ORM\Column(type="string")
*/
public $title;
/**
* global getter
*
* #param type $property
* #return type
*/
public function __get($property) {
return $this->$property;
}
/**
* Global setter
*
* #param type $property
* #param type $value
*/
public function __set($property, $value) {
$this->$property = $value;
}
}
As I'm new to Doctrine and Zend Framework for that mater, I thought I would do a test to see if I could make the Entity Manager save to the database. I setup my code in the indexController as follows:
$em = $this->getLocator()->get('doctrine_em');
$album = new \Application\Entity\Album();
$album->artist = 'Art Ist';
$album->title = 'Cool Title';
$em->persist($album);
$em->flush();
The problem I'm having it that when this code runs I get the following error:
Class Application\Entity\Album is not a valid entity or mapped super class.
From the limited stuff I have found I think the problem has something to do with Doctrine not knowing the entity path or something to do with the AnnotationDriver.
Guessing there is something that needs to be added to the Album module's config file but can't find what.
Updated: Since I don't have enough reputation points to post the answer formally I'll add the answer here.
Found the solution. There were actually two parts to what was going wrong.
First was a stupid mistake, I forgot to take the .dist off the end of the module.spiffy_doctrine_orm.config.php file that gets dropped into the applications config/autoload directory.
Second part was in this file, I didn't alter the driver settings of the settings array to point to:
'driver' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'namespace' => 'AlbumDoc\Entity',
'paths' => array('module/AlbumDoc/src/AlbumDoc/Entity')
)
This does beg the question of whether it would be possible for each module to hold it's own entities, if you are setting a global entity path for your application. But that can wait for another day.
(Anwering the second question)
Yes this is possible and (in my oppinion) the way to go. Your module's config just has to return a subset of the orm-configuration you want to change/extend.
Unfortunatelly Doctrine's drivers don't seem to support multiple namespaces. Therefore you'll have to add a new driver for each namespace (not sure about this, correct me if i'm wrong :)
To add a new driver, let you module's config contain something like:
return array(
'di' => array(
'instance' => array(
'orm_driver_chain' => array(
'parameters' => array(
'drivers' => array(
'mymodule' => array(
'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
'namespace' => __NAMESPACE__ . '\Entity',
'paths' => array(
__DIR__ . '/../src/' . __NAMESPACE__ . '/Entity'
)
)
)
)
)
),
));
This will add a new driver to the configuration without touching any non-module related config-files.