Doctrine 2 ORM DateTime field in identifier - doctrine-orm

In our DB tables, we columns refID and date are a composite primary key, with one field of the identifier being mapped as a datetime:
class corpWalletJournal
{
/**
* #ORM\Column(name="refID", type="bigint", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $refID;
/**
* #ORM\Column(name="date", type="datetime", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $date;
public function setRefID($refID)
{
$this->refID = $refID;
}
public function setDate(\DateTime $date)
{
$this->date = $date;
}
}
If we describe them in entity as #ORM\Id this code will return exception "cannot convert datetime to string"...
$filter = array(
'date' => $this->stringToDate($loopData['date']),
'refID' => $loopData['refID']
));
$oCorpWJ = $this->em->getRepository('EveDataBundle:corpWalletJournal')->findOneBy($filter);
// ...
$oCorpWJ->setDate($this->stringToDate($loopData['date']));
// ...
If we describe corpWalletJournal#date as a simple column, code works fine. Why?
How can we deal with it? We need to have both date and refID in the primary key.
ADDED:
So i created new class
use \DateTime;
class DateTimeEx extends DateTime
{
public function __toString()
{
return $this->format('Y-m-d h:i:s');
}
}
And new type for it
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Eve\DataBundle\Entity\Type\DateTimeEx;
class DateTimeEx extends Type
{
const DateTimeEx = 'datetime_ex';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'my_datetime_ex';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return new DateTimeEx($value);
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $value->format('Y-m-d h:i:s');
}
public function getName()
{
return self::DateTimeEx;
}
public function canRequireSQLConversion()
{
return true;
}
}
How can i use them in entity?
My (edited) Type Class
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class DateTimeEx extends Type
{
const DateTimeEx = 'datetime_ex';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'my_datetime_ex';
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
return $value;
}
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
return $value;
}
public function getName()
{
return self::DateTimeEx;
}
}

Doctrine 2 ORM needs to convert identifier fields to strings in the UnitOfWork. This is required to have the EntityManager being able to track changes to your objects.
Since objects of type DateTime don't natively implement method __toString, converting them to strings is not as simple as casting them to strings.
Therefore, the default date, datetime and time types are not supported as part of the identifier.
To deal with it, you should define your own custom field type mydatetime mapped to your own MyDateTime class which implements __toString. That way, the ORM can handle identifiers also if they contain objects.
Here's an example of how that class may look like:
class MyDateTime extends \DateTime
{
public function __toString()
{
return $this->format('U');
}
}
And here's an example of how the custom DBAL type would look like:
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class MyDateTimeType extends DateTimeType
{
public function convertToPHPValue($value, AbstractPlatform $platform)
{
$dateTime = parent::convertToPHPValue($value, $platform);
if ( ! $dateTime) {
return $dateTime;
}
$val = new MyDateTime('#' . $dateTime->format('U'));
$val->setTimezone($dateTime->getTimezone());
return $val;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
public function getName()
{
return 'mydatetime';
}
}
Then you register it with your ORM configuration during bootstrap (depends on the framework you are using). In symfony, it is documented on the symfony doctrine documentation.
After that, you can use it in your entities:
class corpWalletJournal
{
// ...
/**
* #ORM\Column(name="date", type="mydatetime", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="NONE")
*/
private $date;

Be careful with
return new DateTimeEx('#' . $dateTime->format('U'));
The timezone won't be good. You should do :
$val = new DateTimeEx('#' . $dateTime->format('U'));
$val->setTimezone($dateTime->getTimezone());
return $val;

Based on #Ocramius answer:
namespace App\Interface;
interface StringableDateTimeInterface extends \DateTimeInterface, \Stringable
{
}
namespace App\Type;
use App\Interface\StringableDateTimeInterface;
class DateTime extends \DateTime implements StringableDateTimeInterface
{
public function __toString(): string
{
return $this->format('U');
}
}
namespace App\DBAL;
use App\Type\DateTime;
use Doctrine\DBAL\Types\DateTimeType as DoctrineDateTimeType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class DateTimeType extends DoctrineDateTimeType
{
public function convertToPHPValue($value, AbstractPlatform $platform): mixed
{
$dateTime = parent::convertToPHPValue($value, $platform);
if (!$dateTime) {
return $dateTime;
}
$val = new DateTime('#' . $dateTime->format('U'));
$val->setTimezone($dateTime->getTimezone());
return $val;
}
}
namespace App\DBAL;
use App\Type\DateTime;
use Doctrine\DBAL\Types\DateType as DoctrineDateType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
class DateType extends DoctrineDateType
{
public function convertToPHPValue($value, AbstractPlatform $platform): mixed
{
$dateTime = parent::convertToPHPValue($value, $platform);
if (!$dateTime) {
return $dateTime;
}
$val = new DateTime('#' . $dateTime->format('U'));
$val->setTimezone($dateTime->getTimezone());
return $val;
}
}
then add in config/packages/doctrine.yaml
doctrine:
dbal:
types:
datetime: App\DBAL\DateTimeType
date: App\DBAL\DateType
And finally in your Entity use like:
#[ORM\Id]
#[ORM\Column(type: 'date')]
private ?StringableDateTimeInterface $date;
public function getDate(): ?StringableDateTimeInterface
{
return $this->date;
}
public function setDate(StringableDateTimeInterface $date): self
{
$this->date = $date;
return $this;
}
Don't forget to create new App\Type\DateTime and not DateTime when setting the values of instance.

Related

Duplicate Entity with related translation

I am using Symfony 2.8 with some bundles installed
knplabs/doctrine-behaviors -> this is for softdelete
a2lix/translation-form-bundle -> this is for translation
CLASS Room
namespace AppBundle\Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity;
/**
* Room
* #ORM\Table(name="room")
* #ORM\Entity(repositoryClass="AppBundle\Repository\RoomRepository")
* #Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false)
*/
class Room {
use ORMBehaviors\Translatable\Translatable;
use SoftDeleteableEntity;
private $id;
private $price;
public function getId() {
return $this->id;
}
public function setPrice($price) {
$this->price = $price;
return $this;
}
public function getPrice() {
return $this->price;
}
public function __clone() {
$this->id = null;
}
}
CLASS Room Translation
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model as ORMBehaviors;
use Symfony\Component\Validator\Constraints as Assert;
class RoomTranslation {
use ORMBehaviors\Translatable\Translation;
private $name;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
}
And now on Controller i need to clone a room object with softdelete the original one
my action like that
public function CloneAction(Request $request) {
$em = $this->getDoctrine()->getEntityManager();
$room = $em->getRepository('AppBundle:Room')->findOneById(1);
$new = clone $room;
$new->setPrice(500);
foreach ($room->getTranslations() as $translation) {
$new->addTranslation(clone $translation);
}
$em->remove($room);
$em->persist($new);
$em->flush();
}
When i run this code i got this error
Detached entity AppBundle\Entity\RoomTranslation#00000000293cdfa500000000bfea438f cannot be removed
Any idea why i got this error ?

Cannot get doctrine 2 many-to-many relationships to work

I have 2 entities - a User and a Tag.
This is my user:
<?php
namespace Project\Model;
/**
* #Entity
* #Table(name="users")
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="discr", type="string")
* #DiscriminatorMap({"user" = "User", "client" = "Client", "staff" = "Staff"})
**/
class User implements \JsonSerializable {
/** #Id #Column(type="integer") #GeneratedValue **/
protected $id;
/** #Column(type="string", name="first_name") **/
protected $firstName;
/**
* #ManyToMany(targetEntity="Project\Model\Tag", inversedBy="users")
* #JoinTable(name="user_tags")
**/
protected $tags;
/**
* Construct a new user.
*/
public function __construct() {
$this->tags = new \Doctrine\Common\Collections\ArrayCollection();
}
// Getters
public function getId() {
return $this->id;
}
public function getFirstName() {
return $this->firstName;
}
public function getTags() {
return $this->tags;
}
// Setters
public function setFirstName($firstName) {
$this->firstName = $firstName;
}
/**
* Add a tag to a user.
* #param Tag
*/
public function addTag(Tag $tag) {
$tag->addUser($this);
$this->tags[] = $tag;
}
}
This is my Tag:
<?php
namespace Project\Model;
/**
* #Entity
* #Table(name="tags")
**/
class Tag implements \JsonSerializable {
/** #Id #Column(type="integer") #GeneratedValue **/
protected $id;
/** #Column(type="string") **/
protected $tag;
/**
* #ManyToMany(targetEntity="Project\Model\User", mappedBy="tags")
*/
protected $users;
public function __construct() {
$this->users = new \Doctrine\Common\Collections\ArrayCollection();
}
// Getters
public function getId() {
return $this->id;
}
public function getTag() {
return $this->tag;
}
// Setters
public function setTag($tag) {
$this->tag = $tag;
}
public function addUser(User $user) {
$this->users[] = $user;
}
}
If I create a new Tag, a new User, add the Tag to the User, then call the getTag() method, it returns nothing -- can anybody help me out where I am going wrong?
$tag = new Tag();
$tag->setTag('Foo');
$entityManager->persist($tag);
$user = new User();
$user->addTag($tag);
$entityManager->persist($user);
$entityManger->flush();
var_dump($user->getTags());
I think the problem might come from your ManyToMany relation. You use
#ManyToMany(targetEntity="Tag", inversedBy="users")
whereas you should have something like (assuming you are using Symfony 2 of course) :
#ManyToMany(targetEntity="AppBundle\Entity\Tag")
Also, you use a inversedBybut no mappedBy, so your mapping is invalid.
And this last one is more of a detail, but name a property "tag" inside a Tag class is not the cleanest. Maybe change it to "name".
In your Tag class, you reference the User class using:
/**
* #ManyToMany(targetEntity="Bix\Model\User", mappedBy="tags")
*/
Should "Bix" be "Project"? Unless this was a typo in your question, that woud cause issues.
One side should "own" the association and be responsible for setting the inverse association when it is added.
<?php
// Assuming that the User is the "owning side".
class User {
// Mappings as you have them, minus the "Bix" namespace thing.
public function getTags()
{
return $this->tags;
}
public function addTag(Tag $tag)
{
$tag->addUser($this);
$this->tags->add($tag);
}
public function removeTag(Tag $tag)
{
$tag->removeUser($this);
$this->tags->removeElement($tag);
}
}
class Tag {
// Mappings as you have them, minus the "Bix" namespace thing.
public function getUsers()
{
return $this->users;
}
public function addUser(User $user)
{
$this->users->add($user);
}
public function removeUser(User $user)
{
$this->users->removeElement($user);
}
}

Laravel 4 Mocking Issue : should be called exactly 1 times but called 0 times

I have a problem on unit testing my project.
This is my code:
namespace Way\Storage\HalisahaAccount;
# app/lib/Way/Storage/HalisahaAccount/HalisahaAccountRepositoryInterface.php
interface HalisahaAccountRepositoryInterface {
public function all();
public function find($id);
public function create($input);
}
And the Eloquent repository
namespace Way\Storage\HalisahaAccount;
# app/lib/Way/Storage/HalisahaAccount/EloquentHalisahaAccountRepository.php
use HalisahaAccount;
class EloquentHalisahaAccountRepository implements HalisahaAccountRepositoryInterface {
public function all()
{
return HalisahaAccount::all();
}
public function find($id)
{
return HalisahaAccount::find($id);
}
public function create($input)
{
return HalisahaAccount::create($input);
}
}
And this is my model: HalisahaAccount
class HalisahaAccount extends Eloquent {
/**
* shouldReceive for test
*/
public static function shouldReceive()
{
$class = get_called_class();
$repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface";
$mock = Mockery::mock($repo);
App::instance($repo, $mock);
return call_user_func_array([$mock, 'shouldReceive'], func_get_args());
}
}
Controller
class HalisahalarController extends BaseController {
protected $halisahaAccount;
public function __construct (HalisahaAccount $halisahaAccount) {
$this->halisahaAccount = $halisahaAccount;
}
public function getIndex(){
$halisahalar = $this->halisahaAccount->all();
return View::make('index',array('halisahalar' => $halisahalar));
}
}
And my test
class HalisahalarControllerTest extends TestCase {
public function tearDown(){
Mockery::close();
}
/**
* halısahaların listelendiği sayfanın testi
*/
public function testGetIndex(){
HalisahaAccount::shouldReceive('all')->once();
$this->client->request('GET', '/halisahalar');
$this->assertViewHas('halisahalar');
}
}
I am running the phpunit test, but getting the this error:
1) HalisahalarControllerTest::testGetIndex
Mockery\Exception\InvalidCountException: Method all() from Mockery_0_Way_Storage_HalisahaAccount_HalisahaAccountRepositoryInterface should be called
exactly 1 times but called 0 times.
Why the mockery dont call my all() method?

ZF2 How to inject Service Locator or Plugins in Abstract Entity Class

I have an abstract class for my Doctrine 2 Entity. How do I inject the Service Locator to get for example the Translator or Zend\Mvc\Controller\Plugin\Url or how do I directly inject these plugins to the abstract entity class.
The goal is to get the doctrine entity from the entity repository and manipulate the result of the entity within an abstract entity model/service.
Doctrine 2 Entity in short form:
namespace Rental\Entity;
use Doctrine\ORM\Mapping as ORM;
use Rental\Model\Rental as AbstractRental;
/**
* Rental
*
* #ORM\Entity(repositoryClass="Rental\Repository\Rental") *
* #ORM\Table(name="rental", options={"collate"="utf8_general_ci"})
*/
class Rental extends AbstractRental{
...
public function getType(){
...
}
... entity setter and getter
}
Abstract entity model:
namespace Rental\Model;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
/**
* Rental\Model\Rental
* #ORM\MappedSuperclass
* #ORM\HasLifecycleCallbacks
*/
abstract class Rental implements ServiceLocatorAwareInterface
{
protected $serviceLocator;
protected $translator;
abstract protected function getType();
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function getTranslator()
{
if (!$this->translator) {
$this->translator = $this->getServiceLocator()->get('translator');
// here is the problem, because getServiceLocator is NULL
}
return $this->translator;
}
public function getTranslatedType(){
return $this->translator->translate($this->getType())
}
This is not working because the abstract class is not instantiated and so the ServiceLocatorInterface is not injected.
Here is my Controller:
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Doctrine\ORM\EntityManager;
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Doctrine\ORM\EntityManager;
class IndexController extends AbstractActionController
{
/**
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager(EntityManager $em)
{
$this->em = $em;
}
/**
* #return array|\Doctrine\ORM\EntityManager|object
*/
public function getEntityManager()
{
if (NULL === $this->em) {
$this->em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
return $this->em;
}
/**
* #return \Zend\View\Model\ViewModel
*/
public function indexAction()
{
...
/** #var $repository \Rental\Repository\Rental */
$repository = $this->getEntityManager()->getRepository('Rental\Entity\Rental');
/** #var $rentals array */
$rental= $repository->findBySlug($slug);
\ChromePhp::log($rental->getTranslatedType());
// is NULL
This is one way to accomplish your goal of injecting the ZF2 Service Locator into your entities that inherit from the abstract class
Remember to change/fix any namespaces
First, you'll need to register an event listener with Doctrine. You only need to listen for postLoad. To do this, instantiate the listener (which we'll define next) and pass it it's sole dependency, which is the service locator.
Module.php
// in your Module.php
use Rental\Model\Listeners\EntityInjectorListener;
public function onBootstrap(MvcEvent $e)
{
$serviceLocator = $e->getApplication()->getServiceManager();
$entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');
$entityManager->getEventManager()->addEventListener(array(\Doctrine\ORM\Events::postLoad), new EntityInjectorListener($serviceLocator));
}
Now define a listener for Doctrine to use. It should check the entity it's working on and if it inherits from your abstract class, then it should set the service locator.
// EntityInjectorListener.php
namespace \Rental\Model\Listeners;
class EntityInjectorListener
{
protected $serviceLocator;
public function __construct($serviceLocator)
{
$this->serviceLocator = $serviceLocator;
}
public function postLoad($eventArgs)
{
// check if entity is a child of abstract class
if ($eventArgs->getEntity() instanceof \Rental\Model\Rental) {
$eventArgs->getEntity()->setServiceLocator($this->serviceLocator);
}
}
}
Now within your entity you should be able to call
$this->getServiceLocator()->get('whatever-you-want-get');
To use view helpers you will to need call them like this:
$this->getServiceLocator()->get('ViewHelperManager')->get('url');

how to get entity manager in view helper using doctrine2 zf2

Following code is working fine with AbstractPlugin but I need to access entity manager in view helper. How can I get entity manager in view helper?
Is there way to save entityManager instance somewhere in registry so that I can access it whereever i want? would that be a good practice?
use Zend\View\Helper\AbstractHelper;
use Doctrine\ORM\EntityManager;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\ServiceManager\ServiceManager;
class IsAuthz extends AbstractHelper implements ServiceManagerAwareInterface
{
/*
* #var Doctrine\ORM\EntityManager
*/
protected $em;
protected $sm;
public function __construct($e) {
$app = $e->getParam('application');
$em = $this->getEntityManager();
}
public function __invoke()
{
return $this;
}
/**
* #return Doctrine\ORM\EntityManager
*/
public function getEntityManager()
{
if (null === $this->em) {
$this->em = $this->sm->getServiceLocator()->get('doctrine.entitymanager.orm_default');
}
return $this->em;
}
/**
*
* #param \Doctrine\ORM\EntityManager $em
*/
public function setEntityManager(EntityManager $em)
{
$this->em = $em;
}
/**
* Retrieve service manager instance
*
* #return ServiceManager
*/
public function getServiceManager()
{
return $this->sm->getServiceLocator();
}
/**
* Set service manager instance
*
* #param ServiceManager $locator
* #return void
*/
public function setServiceManager(ServiceManager $serviceManager)
{
$this->sm = $serviceManager;
}
}
ERROR:
Fatal error: Call to a member function getServiceLocator() on a non-object in XXX/XXX/src/XXX/View/Helper/IsAuthz.php on line 41
As "Daniel M" said, it is best to use the service to keep the data logic separate. but someone still want to use data logic in view helper, he can do it as below. I have changed the above code in question as follow.
public function __construct($e, $sm) {
$app = $e->getParam('application');
$this->sm = $sm;
$em = $this->getEntityManager();
}
and then I inject the $sm object from module.php using
public function onBootstrap($e)
{
$serviceManager->get('viewhelpermanager')->setFactory('IsAuthz', function ($sm) use ($e) {
return new \xxx\View\Helper\IsAuthz($e, $sm);
});
}
better way is to use service in zend framework 2 for this purpose.