I am looking for a way to convert an array to doctrine Entity. I am using doctrine 2.
I have an entity class like:
class User
{
/**
* #Id
* #Column(type="integer", nullable=false)
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #Column(type="string", length=255, unique=true, nullable=false)
*/
protected $email;
/**
* #Column(type="string", length=64, nullable=false)
*/
protected $password;
/**
* #var DateTime
* #Column(type="datetime", nullable=false)
*/
protected $dob;
//getter and setters
}
When I post data from a html form, I want to convert the post array to the User entity. So I have an array like
$userAsArray = array("email"=>"abc#xyz.com","password"=>"hello","dob"=>"10\20\1990");
$user = new User();
covert($userAsArray,$user) // I am looking for something like this
I am looking for a generic way to accomplish this. I have tried something like this:
function fromArray(array $array,$class){
$user = new $class();
foreach($array as $key => $field){
$keyUp = ucfirst($key);
if(method_exists($user,'set'.$keyUp)){
call_user_func(array($user,'set'.$keyUp),$field);
}
}
return $user;
}
But the problem is it sets everything as string. But for the date I want to have it as a DateTime object. Any help?
what if one of your array elements is a foreign key ? before setting entity properties, you might need to prepare foreign key attributes. This is how I accomplish similar task:
Extend Repository
<?php
namespace My\Doctrine;
use Doctrine\ORM\EntityRepository;
class Repository extends EntityRepository
{
/**
* Prepare attributes for entity
* replace foreign keys with entity instances
*
* #param array $attributes entity attributes
* #return array modified attributes values
*/
public function prepareAttributes(array $attributes)
{
foreach ($attributes as $fieldName => &$fieldValue) {
if (!$this->getClassMetadata()->hasAssociation($fieldName)) {
continue;
}
$association = $this->getClassMetadata()
->getAssociationMapping($fieldName);
if (is_null($fieldValue)) {
continue;
}
$fieldValue = $this->getEntityManager()
->getReference($association['targetEntity'], $fieldValue);
unset($fieldValue);
}
return $attributes;
}
}
Create parent Entity class :
namespace My\Doctrine;
class Entity
{
/**
* Assign entity properties using an array
*
* #param array $attributes assoc array of values to assign
* #return null
*/
public function fromArray(array $attributes)
{
foreach ($attributes as $name => $value) {
if (property_exists($this, $name)) {
$methodName = $this->_getSetterName($name);
if ($methodName) {
$this->{$methodName}($value);
} else {
$this->$name = $value;
}
}
}
}
/**
* Get property setter method name (if exists)
*
* #param string $propertyName entity property name
* #return false|string
*/
protected function _getSetterName($propertyName)
{
$prefixes = array('add', 'set');
foreach ($prefixes as $prefix) {
$methodName = sprintf('%s%s', $prefix, ucfirst(strtolower($propertyName)));
if (method_exists($this, $methodName)) {
return $methodName;
}
}
return false;
}
}
Usage, a method in your repo:
$entity = new User();
$attributes = array(
"email" =>"abc#xyz.com",
"password" =>"hello",
"dob" =>"10\20\1990"));
$entity->fromArray($this->prepareAttributes($attributes));
$this->getEntityManager()->persist($entity);
$this->getEntityManager()->flush();
Why not write your setDob() method to detect a string and convert it if necessary
public function setDob($dob) {
if (!$dob instanceof DateTime) {
$dob = new DateTime((string) $dob); // or however you want to do the conversion
}
$this->dob = $dob;
}
You are trying to de-serialize
I would check out the Zend Framework 2 Stdlib component. You do not need to use the whole framework.
The Hydrators, specifically the DoctrineModule\Stdlib\Hydrator\DoctrineObject, do what you are asking.
Related
I am currently using doctrine merge to "restore" an entity with relationships after retrieving it from the session.
As from doctrine 3, this function will be deprecated so I am wondering if there is any way to keep an entity object in the session for a while before persisting it to the database.
I need this for a multistep form through which my object gets populated.
For now, the only solution i see is storing the entity in a temporary database table but i don't really like this idea because my table will be filled with "junk".
Thanks !
There are two ways that I have found.
The first is to create your own implementation. I have go this way because there were a lot of usages of merge in project. It looks hacky, but works:
class DoctrineMergeService
{
/**
* #var EntityManager
*/
private $em;
/**
* #param EntityManager $em
*/
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* #param object $entity
*
* #return object
*
* #throws \Doctrine\ORM\ORMException
* #throws \Doctrine\ORM\OptimisticLockException
* #throws \Doctrine\ORM\TransactionRequiredException
*/
public function merge(object $entity): object
{
$mergedEntity = null;
$className = get_class($entity);
$identifiers = $this->getIdentifiersFromEntity($entity);
$entityFromDoctrine = $this->em->find($className, $identifiers);
if ($entityFromDoctrine) {
$mergedEntity = $this->mergeEntities($entityFromDoctrine, $entity);
} else {
$this->em->persist($entity);
$mergedEntity = $entity;
}
return $mergedEntity;
}
/**
* #param object $entity
*
* #return array
*/
private function getIdentifiersFromEntity(object $entity): array
{
$className = get_class($entity);
$meta = $this->em->getClassMetadata($className);
$identifiers = $meta->getIdentifierValues($entity);
return $identifiers;
}
/**
* #param object $first
* #param object $second
*
* #return object
*/
private function mergeEntities(object $first, object $second): object
{
$classNameFirst = get_class($first);
$metaFirst = $this->em->getClassMetadata($classNameFirst);
$classNameSecond = get_class($second);
$metaSecond = $this->em->getClassMetadata($classNameSecond);
$fieldNames = $metaFirst->getFieldNames();
foreach ($fieldNames as $fieldName) {
$secondValue = $metaSecond->getFieldValue($second, $fieldName);
$metaFirst->setFieldValue($first, $fieldName, $secondValue);
}
return $first;
}
}
The second is to use serializer, not tested:
// this is controller or something like controller
public function save($id)
{
$serializedJsonFromSession = $this->session->get('serialized_json');
$doctrine = $this->getDoctrine();
$entity = $doctrine->getRepository(Entity::class)->find($id);
if (!$entity) {
$entity = new Entity();
$doctrine->persist($entity);
}
$serializer->deserialize(
$serializedJsonFromSession,
Entity::class,
'json',
[AbstractNormalizer::OBJECT_TO_POPULATE => $entity]
);
$doctrine->flush();
}
I have two entities, for products and translations:
class ProductEntity
{
/**
* #Id
* #var string
* #Column(type="string", length=3)
*/
protected $code;
/**
* #OneToMany(targetEntity="ProductTranslationEntity", mappedBy="product")
*/
private $translations;
public function __construct()
{
$this->translations = new ArrayCollection();
}
/.../ getters and setters
public function addTranslation(ProductTranslationEntity $productTranslation)
{
$this->translations->add($productTranslation);
}
public function clearTranslations()
{
$this->translations->clear();
}
}
.
class ProductTranslationEntity
{
/**
* #ManyToOne(targetEntity="ProductEntity", inversedBy="translations")
* #JoinColumn(name="product_code", referencedColumnName="code", onDelete="CASCADE")
* #Id
*/
private $product;
/**
* #var string
* #Column(type="string", name="language_code", length=5)
* #Id
*/
protected $languageCode;
/**
* #var string
* #Column(type="string", name="product_name", length=128)
*/
protected $productName;
/.../ getters and setters
}
I like to replace all translations with new ones, from array like that:
['en' => ['name' => 'name_en'], 'de' => ['name' => 'name_de']];
Because in this array I have set of all supported languages the bast way I can see is to remove all existing translations and put new ones:
$product // Existing product entity
$product->clearTranslations();
$this->entityManager->flush($product);
foreach ($translations as $code => $translation) {
$t = new ProductTranslationEntity();
$t->setProduct($product);
$t->setLanguageCode($code);
$t->setProductName($translation['name']);
$this->entityManager->persist($t);
$product->addTranslation($t);
}
$this->entityManager->flush($product);
This solution doesn't work because after first $this->entityManager->flush($product); there are still translations in database so i get error about duplicates.
What have I done wrong in my solution? Or maybe there is another way to solve this kind of issue?
As Doctrine documentation refers:
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine's point of view).
So to properly clear the translations of a product you should change the clearTranslations() function inside Product entity to:
public function clearTranslations()
{
foreach ($this->translations as $translation) {
$translation->setProduct(null);
}
$this->translations->clear();
}
so that you also update the owning side of the association before removal.
This maybe a bit overload but still it does not use extra requests to database:
$current_translations = $product->getTranslations();
foreach ($translations as $code => $translation) {
$translation_found = false;
foreach ($current_translations as $current_translation) {
if ($current_translation->getLanguageCode() === $code) {
// you've found the translation - replace value
$current_translation->setProductName($translation['name']);
$translation_found = true;
break;
}
}
if (!$translation_found) {
// translation with such code not found - add a new one
$t = new ProductTranslationEntity();
$t->setProduct($product);
$t->setLanguageCode($code);
$t->setProductName($translation['name']);
$this->entityManager->persist($t);
$product->addTranslation($t);
}
}
$this->entityManager->persist($product);
Use orphanRemoval=true:
/**
* #OneToMany(targetEntity="ProductTranslationEntity", mappedBy="product", orphanRemoval=true)
*/
private $translations;
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 have the following two classes (I have not included the interfaces)
ConditionsRefer
namespace PG\Referrer\Single\Post;
class ConditionsRefer implements ConditionsReferInterface
{
/**
* #var $authorReferrer = null
*/
private $isAuthorReferrer = null;
/**
* #var $dateReferrer = null
*/
private $isDateReferrer = null;
/**
* #var $searchReferrer = null
*/
private $isSearchReferrer = null;
/**
* #var $taxReferrer = null
*/
private $isTaxReferrer = null;
/**
* #param array $values = null;
*/
public function __construct(array $values = null)
{
if ($values)
$this->setBulk($values);
}
/**
* Bulk setter Let you set the variables via array or object
*/
public function setBulk($values)
{
global $wp_query;
if (!is_array($values) && !$values instanceof \stdClass) {
throw new \InvalidArgumentException(
sprintf(
'%s needs either an array, or an instance of \\stdClass to be passed, instead saw %s',
__METHOD__,
is_object($values) ? get_class($values) : gettype($values)
)
);
}
foreach ($values as $name => $value) {//create setter from $name
if (array_key_exists($value, $wp_query->query_vars)) { //Check that user don't set a reserved query vars
throw new \InvalidArgumentException(
sprintf(
'%s is a reserved query_vars and cannot be used. Please use a unique value',
$value
)
);
}
$setter = 'set' . $name;
$condition = isset($_GET[$value]);
if ($setter !== 'setBulk' && method_exists($this, $setter)) {
$this->{$setter}($condition);//set value (bool)
}
}
return $this;
}
/**
* #param $authorReferrer
* #return $this
*/
public function setAuthorReferrer($isAuthorReferrer)
{
$this->isAuthorReferrer = $isAuthorReferrer;
return $this;
}
/**
* #param $dateReferrer
* #return $this
*/
public function setDateReferrer($isDateReferrer)
{
$this->isDateReferrer = $isDateReferrer;
return $this;
}
/**
* #param $searchReferrer
* #return $this
*/
public function isSearchReferrer($isSearchReferrer)
{
$this->isSearchReferrer = $isSearchReferrer;
return $this;
}
/**
* #param $taxReferrer
* #return $this
*/
public function setTaxReferrer($isTaxReferrer)
{
$this->isTaxReferrer = $isTaxReferrer;
return $this;
}
}
QueryArgumentsRefer
namespace PG\Referrer\Single\Post;
class QueryArgumentsRefer implements QueryArgumentsReferInterface
{
private $referrer;
public function __construct(ConditionsReferInterface $referrer, array $values = array())
{
$this->referrer = $referrer;
$this->referrer->setBulk($values);
}
public function getReferrer()
{
return $this->referrer;
}
public function b()
{
$test = (object) $this->referrer;
if($test->isAuthorReferrer === false)
return 'This is just a test';
}
}
This is how I use it in a file
$a = new QueryArgumentsRefer(new ConditionsRefer(), ['authorReferrer' => 'aq']);
?><pre><?php var_dump($a->b()); ?></pre><?php
In function b() in class QueryArgumentsRefer, I need to use the properties of class ConditionsRefer.
This is the result of $test, which is the expected result, so this is working
object(PG\Referrer\Single\Post\ConditionsRefer)#522 (4) {
["isAuthorReferrer":"PG\Referrer\Single\Post\ConditionsRefer":private]=>
bool(false)
["isDateReferrer":"PG\Referrer\Single\Post\ConditionsRefer":private]=>
NULL
["isSearchReferrer":"PG\Referrer\Single\Post\ConditionsRefer":private]=>
NULL
["isTaxReferrer":"PG\Referrer\Single\Post\ConditionsRefer":private]=>
NULL
}
If I try to use $test->isAuthorReferrer, I get the following error
Fatal error: Cannot access private property PG\Referrer\Single\Post\ConditionsRefer::$isAuthorReferrer
which is expected I guess. The only way to make this work in my mind is setting the properties in ConditionsRefer to public
I've read properties should be private, and not public. How can I properly work around this problem, or do I have to make my properties public
EDIT
I have tried setting my properties to protected, but that does noet help as this also gives me a fatala error
Use protected and your child classes can use it. This way you can still have access to it in children classes without making it public. private means only the base class may use it.
Also, as a side note, all variables in a class without a default value will default to null
/**
* #var $authorReferrer = null
*/
protected $isAuthorReferrer;
Solve this issue. I'm still new to OOP and had a slight misunderstanding about setters and getter.
What I did is, I created a getter for each setter in the ConditionsRefer class, and instead of trying to use the properties of this class in the QueryArgumentsRefer class (which caused the initial error), I used the getters to get my info from the ConditionsRefer class inside the QueryArgumentsRefer class like
$this->conditionalReferrer->isAuthorReferrer();
Suppose i'm having the following Doctrine 2 entity:
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*
* #var int
*/
protected $id;
/**
* #ORM\Column(length=100)
*
* #var string
*/
protected $name;
/**
* #ORM\Column(type="integer")
*
* #var int
*/
protected $status;
}
The User can have several statuses, for example: Pending, Active, Suspended. These statuses are needed throughout the code (services, repositories, etc.) and also in the UI layer (a User edit form would display them in a dropdown).
In order to avoid defining them in multiple places, what i've done so far was to use a class to hold them all (all the application's constants), and it looks somewhat like this:
class App_Constants extends Zrzr_Constants
{
protected static $_constants = array(
'users' => array(
'status' => array(
0 => 'Pending',
1 => 'Active',
2 => 'Suspended' ) ) );
}
The base class (Zrzr_Constants) would offer some methods to retrieve them, and it looks like this:
class Zrzr_Constants
{
protected static $_constants = array();
public static function getConstantValues( $key, $subkey )
{
// ...
}
public static function getConstantByName( $name )
{
// ...
}
}
Common usage would be:
// example of retrieval by constant name ... it would return an integer
$pendingStatus = App_Constants::getConstantByName( 'USERS.STATUS.PENDING' );
// example of retrieval for UI display purposes ... would return an array
$statuses = App_Constants::getConstantValues('users', 'status');
Of course this means that there are some limitations in that the constant labels cannot contain dots, but i can live with it.
Using Doctrine 2 and going the DDD way however, tells me that the 'status' field should be in fact a 'value object' (but Doctrine 2 does not support value objects yet), or at least that i should have the constants defined within the entity (using const).
My question is how would i do this so that i avoid constant redefinition for the UI layer? I need to have access to the constant by name (in the code) and to have all the possible values for such a field in the case of a UI dropdown (for example).
I think, you can do it this way:
class User {
const STATUS_PENDING = 'Pending';
const STATUS_ACTIVE = 'Active';
const STATUS_SUSPENDED = 'Suspended';
public static function getStatusList() {
return array(
self::STATUS_PENDING,
self::STATUS_ACTIVE,
self::STATUS_SUSPENDED
);
}
public function getStatus() {...}
public function setStatus($value) {...}
public function isStatusPending() {...} //If you need it
}
On the UI layer, you can get text versions of your statuses using localization service (if status constants are numbers, UI layer can convert them to strings by adding prefix, for example user_status_0). In Symfony2 views you can use trans Twig filter for that to get text version of user status from user localization domain.
If your website is just in one language, then just User::STATUS_XXX will do fine, I think. I don't think you should overcomplicate the matter by creating a new class to hold statuses of the user.
If you will end up having many statuses or some other related things, I think you will have to create a separate entity for them.
you can define your class as in the following example
class ContactResource
{
const TYPE_PHONE = 1;
const TYPE_EMAIL = 2;
const TYPE_BIRTDAY = 3;
const TYPE_ADDRESS = 4;
const TYPE_OTHER = 5;
const TYPE_SKYPE = 6;
const TYPE_LINKEDIN = 7;
const TYPE_MEETUP = 8;
const TYPE_TELEGRAM = 9;
const TYPE_INSTAGRAM = 10;
const TYPE_TWITTER = 11;
public static $resourceType = array(
ContactResource::TYPE_PHONE => "Phone",
ContactResource::TYPE_EMAIL => "Email",
ContactResource::TYPE_BIRTDAY => "Birtday",
ContactResource::TYPE_ADDRESS => "Address",
ContactResource::TYPE_OTHER => "Other",
ContactResource::TYPE_SKYPE => "Skype",
ContactResource::TYPE_LINKEDIN => "LinkedIn",
ContactResource::TYPE_MEETUP => "Meetup",
ContactResource::TYPE_TELEGRAM => "Telegram",
ContactResource::TYPE_INSTAGRAM => "Instagram",
ContactResource::TYPE_TWITTER => "Twitter",
);
/**
* #var integer
*
* #ORM\Column(type="integer", length=2)
*
*/
private $type;
public function __toString()
{
return (string)$this->getType();
}
public function getType()
{
if (!is_null($this->type)) {
return self::$resourceType[$this->type];
} else {
return null;
}
}
public static function getTypeList() {
return self::$resourceType;
}
}
If you need to get the type in Twig
{{ entity.type }}
For the list of choices
ContactResource::getTypeList()
Hope works for you!
Several years later and some more experience, what I consider to be the proper answer has changed. The initial question is about domain constants used in the UI layer, but the given example and the discussions actually refer to the following concepts: enums, enum maps and value objects. I did not have these concepts back then and the answers to my question did not provide them.
When you see or think of constants like STATUS_PENDING, STATUS_ACTIVE, STATUS_SUSPENDED you should be thinking of an enum. The standard PHP enum is insufficient so I like to use a third party library like marc-mabe/php-enum. Here's how it would look like:
use MabeEnum\Enum;
/**
* #method static UserStatus PENDING()
* #method static UserStatus ACTIVE()
* #method static UserStatus SUSPENDED()
*/
class UserStatus extends Enum
{
const PENDING = 0;
const ACTIVE = 1;
const SUSPENDED = 2;
}
It's easy to turn this into a value object if you need to add functionality to it (I recommend doing it through composition, not inheritance). Coming back to the User entity, using the above enum the entity would end up like this:
/**
* #ORM\Entity
* #ORM\Table(name="users")
*/
class User {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
*
* #var int
*/
protected $id;
/**
* #ORM\Column(length=100)
*
* #var string
*/
protected $name;
/**
* #ORM\Column(type="user_status")
*
* #var UserStatus
*/
protected $status;
}
Notice the column type is "user_status". To get this to work you need to define a custom Doctrine type and register it with Doctrine. Such a type would look like this:
/**
* Field type mapping for the Doctrine Database Abstraction Layer (DBAL).
*
* UserStatus fields will be stored as an integer in the database and converted back to
* the UserStatus value object when querying.
*/
class UserStatusType extends Type
{
/**
* #var string
*/
const NAME = 'user_status';
/**
* {#inheritdoc}
*
* #param array $fieldDeclaration
* #param AbstractPlatform $platform
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getIntegerTypeDeclarationSQL($fieldDeclaration);
}
/**
* {#inheritdoc}
*
* #param string|null $value
* #param AbstractPlatform $platform
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (empty($value)) {
return null;
}
if ($value instanceof UserStatus) {
return $value;
}
try {
$status = UserStatus::get((int)$value);
} catch (InvalidArgumentException $e) {
throw ConversionException::conversionFailed($value, self::NAME);
}
return $status;
}
/**
* {#inheritdoc}
*
* #param UserStatus|null $value
* #param AbstractPlatform $platform
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if (empty($value)) {
return null;
}
if ($value instanceof UserStatus) {
return $value->getValue();
}
throw ConversionException::conversionFailed($value, self::NAME);
}
/**
* {#inheritdoc}
*
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*
* #param AbstractPlatform $platform
*
* #return boolean
*/
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
Finally, when it comes to satisfying the needs of the user interface, you can end up using enum maps. Remember that the UI could need additional functionality such as multiple language support, so you cannot mash such concerns into the domain, hence the separation:
use MabeEnum\EnumMap;
class UserStatusMap extends EnumMap
{
public function __construct()
{
parent::__construct(UserStatus::class);
$this[UserStatus::PENDING] = ['name' => 'Pending'];
$this[UserStatus::ACTIVE] = ['name' => 'Active'];
$this[UserStatus::SUSPENDED] = ['name' => 'Suspended'];
}
}
You can just add as many keys you want beside 'name'. In the UI you can make use of such a map like this:
// if you want to display the name when you know the value
echo (new UserStatusMap ())[UserStatus::PENDING]['name'];
// or
echo (new UserStatusMap ())[UserStatus::PENDING()]['name'];
// if you want to build a list for a select (value => name)
$list = (new UserStatusMap ())->toArray('name');
The toArray function is not available in MabeEnum\EnumMap but you can make your own:
use MabeEnum\EnumMap as BaseEnumMap;
class EnumMap extends BaseEnumMap
{
/**
* #param string|null $metadataKey
*
* #return array
*/
public function toArray($metadataKey = null)
{
$return = [];
$flags = $this->getFlags();
$this->setFlags(BaseEnumMap::KEY_AS_VALUE | BaseEnumMap::CURRENT_AS_DATA);
if ($metadataKey) {
foreach ($this as $key => $value) {
$return[$key] = $value[$metadataKey];
}
} else {
$return = iterator_to_array($this, true);
}
$this->setFlags($flags);
return $return;
}
}
To summarize:
Use an Enum to define a list of alternative values for a single field.
Create a Value Object which receives this Enum in the constructor if you want to add VO specific functionality to this field.
Use an Enum Map to serve the UI needs.