ZF2, Doctrine2, Gedmo - SoftDelete JTI Entity with associations - doctrine-orm

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?

Related

How to instantiate new objects from Table Inhertitance in Laminas API?

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

Make a Stub for nesting function in Yii2

I learn to use Unit test in Yii2 with Codeception, and try to check if a billing is a "daily" billing from a merchant's setting.
$this->biller->merchant->detail->rule_type == self::PERIODIC_MODE_DAILY
I don't know how to mock that rule_type value if I use a Stub::make() function.
What I tried so far is using the nested array like this (doesn't work) :
$billing = Stub::make(Billing::class, [
'status' => Billing::STATUS_ACTIVE,
'set_periodic_by' => Billing::SET_PERIODIC_BY_MERCHANT,
'biller' => [
'merchant' => [
'detail' => [
'rule_type' => Billing::PERIODIC_MODE_DAILY,
]
]
]
]);
And I also tried to mock each of the object model using another Stub::make()
$billing = Stub::make(Billing::class, [
'status' => Billing::STATUS_ACTIVE,
'set_periodic_by' => Billing::SET_PERIODIC_BY_MERCHANT,
'getBiller' => Stub::make(Biller::class, [
'getMerchant' => Stub::make(Merchant::class, [
'getDetail' => Stub::make(MerchantDetail::class, [
'rule_type' => Billing::PERIODIC_MODE_DAILY,
])
])
])
]);
How do I properly create a "nested" function return values using Stub? Any comment or answer is always welcome.
Casting the nested array to (object) will do
$billing = Stub::make(Billing::class, [
'status' => Billing::STATUS_ACTIVE,
'set_periodic_by' => Billing::SET_PERIODIC_BY_MERCHANT,
'getBiller' => (object) [
'merchant' => (object) [
'detail' => (object) [
'rule_type' => Billing::PERIODIC_MODE_DAILY,
]
]
]
]);

zfcUser, BjyAuthorize, doctrine orm not working -error: ServiceManager::unable to fetch ObjectManager

i am trying to use BjyAuthroize with my zfcUser and doctrine 2 ORM
as the clue to bind doctrine to bjyauthorize i used
i use the module samUser
in my
however i am getting the following error messages;
Uncaught exception
'Zend\ServiceManager\Exception\ServiceNotFoundException' with message
'Zend\ServiceManager\ServiceManager::get was unable to fetch or create
an instance for My\Doctrine\Common\Persistence\ObjectManager' in
C:\wamp\www\testChat\vendor\zendframework\zend-servicemanager\src\ServiceManager.php
in my config authoload bjyauthorize.global.php file
return [
'bjyauthorize' => [
// set the 'guest' role as default (must be defined in a role provider)
'default_role' => 'guest',
/* this module uses a meta-role that inherits from any roles that should
* be applied to the active user. the identity provider tells us which
* roles the "identity role" should inherit from.
* for ZfcUser, this will be your default identity provider
*/
'identity_provider' => \BjyAuthorize\Provider\Identity\ZfcUserZendDb::class,
/* If you only have a default role and an authenticated role, you can
* use the 'AuthenticationIdentityProvider' to allow/restrict access
* with the guards based on the state 'logged in' and 'not logged in'.
*
* 'default_role' => 'guest', // not authenticated
* 'authenticated_role' => 'user', // authenticated
* 'identity_provider' => \BjyAuthorize\Provider\Identity\AuthenticationIdentityProvider::class,
*/
/* role providers simply provide a list of roles that should be inserted
* into the Zend\Acl instance. the module comes with two providers, one
* to specify roles in a config file and one to load roles using a
* Zend\Db adapter.
*/
'role_providers' => [
/* here, 'guest' and 'user are defined as top-level roles, with
* 'admin' inheriting from user
*/
\BjyAuthorize\Provider\Role\Config::class => [
'guest' => [],
'user' => ['children' => [
'admin' => [],
]],
],
// this will load roles from the user_role table in a database
// format: user_role(role_id(varchar], parent(varchar))
\BjyAuthorize\Provider\Role\ZendDb::class => [
'table' => 'user_role',
'identifier_field_name' => 'id',
'role_id_field' => 'role_id',
'parent_role_field' => 'parent_id',
],
// this will load roles from
// the 'BjyAuthorize\Provider\Role\ObjectRepositoryProvider' service
\BjyAuthorize\Provider\Role\ObjectRepositoryProvider::class => [
// class name of the entity representing the role
'role_entity_class' => 'My\Role\Entity',
// service name of the object manager
'object_manager' => 'My\Doctrine\Common\Persistence\ObjectManager',
],
],
// resource providers provide a list of resources that will be tracked
// in the ACL. like roles, they can be hierarchical
'resource_providers' => [
\BjyAuthorize\Provider\Resource\Config::class => [
'pants' => [],
],
],
/* rules can be specified here with the format:
* [roles (array], resource, [privilege (array|string], assertion])
* assertions will be loaded using the service manager and must implement
* Zend\Acl\Assertion\AssertionInterface.
* *if you use assertions, define them using the service manager!*
*/
'rule_providers' => [
\BjyAuthorize\Provider\Rule\Config::class => [
'allow' => [
// allow guests and users (and admins, through inheritance)
// the "wear" privilege on the resource "pants"
[['guest', 'user'], 'pants', 'wear'],
],
// Don't mix allow/deny rules if you are using role inheritance.
// There are some weird bugs.
'deny' => [
// ...
],
],
],
/* Currently, only controller and route guards exist
*
* Consider enabling either the controller or the route guard depending on your needs.
*/
'guards' => [
/* If this guard is specified here (i.e. it is enabled], it will block
* access to all controllers and actions unless they are specified here.
* You may omit the 'action' index to allow access to the entire controller
*/
\BjyAuthorize\Guard\Controller::class => [
['controller' => 'index', 'action' => 'index', 'roles' => ['guest','user']],
['controller' => 'index', 'action' => 'stuff', 'roles' => ['user']],
// You can also specify an array of actions or an array of controllers (or both)
// allow "guest" and "admin" to access actions "list" and "manage" on these "index",
// "static" and "console" controllers
[
'controller' => ['index', 'static', 'console'],
'action' => ['list', 'manage'],
'roles' => ['guest', 'admin'],
],
[
'controller' => ['search', 'administration'],
'roles' => ['staffer', 'admin'],
],
['controller' => 'zfcuser', 'roles' => []],
// Below is the default index action used by the ZendSkeletonApplication
// ['controller' => 'Application\Controller\Index', 'roles' => ['guest', 'user']],
],
/* If this guard is specified here (i.e. it is enabled], it will block
* access to all routes unless they are specified here.
*/
\BjyAuthorize\Guard\Route::class => [
['route' => 'zfcuser', 'roles' => ['user']],
['route' => 'zfcuser/logout', 'roles' => ['user']],
['route' => 'zfcuser/login', 'roles' => ['guest']],
['route' => 'zfcuser/register', 'roles' => ['guest']],
// Below is the default index action used by the ZendSkeletonApplication
['route' => 'home', 'roles' => ['guest', 'user']],
],
],
],
];
My\Doctrine\Common\Persistence\ObjectManager is just a placeholder for whatever service you will use.
Where you write you are using Doctrine, you need to specify whatever your object manager is called.
For example, you could probably use
doctrine.entitymanager.orm_default
Once you fix this, you will probably notice that you have other placeholders in there too. For example, My\Role\Entity.
I recommend you take a good look at:
https://github.com/bjyoungblood/BjyAuthorize
Good luck! This at least, will fix your object manager naming issue.

Zend Framework 2 + Doctrine2 - Entities not detected

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.

Extending the user entity of ZfcUserDoctrineORM in ZF2

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 ;)