A price object has three properties,
/** #var float */
public $amount = 0.0;
/** #var string */
public $currency = '';
/**
* #var \DateTime
*/
public $dueDate;
When serializing this object to json via the symfony2 Symfony\Component\HttpFoundation\JsonResponse, it will look like:
{
"price": {
"amount": 235,
"currency": "EUR",
"dueDate": {
"date": "2015-10-25 00:00:00.000000",
"timezone": "UTC",
"timezone_type": 3
}
}
}
I want the \DateTime to be formatted as simply a string:
"dueDate": "2015-10-22 00:00:00.000000"
How to get this done is not the scope of the question, as I currently handle this case in the object's constructor:
/**
* Price constructor.
* #param float $amount
* #param string $currency
* #param \DateTime|null $dueDate
*/
public function __construct($amount = 0.0, $currency = "", $dueDate)
{
$this->amount = $amount;
$this->currency = $currency;
$this->dueDate = $dueDate;;
if ($dueDate instanceof \DateTime) {
$this->dueDate = $dueDate->format(\DateTime::ATOM);
}
}
yet it doesn't feel entirely right, and I am curious if I could configure the serialize process differently, in the sense, instead of coding my representation, modify the way the object is serialized.
Reasoning is to have all \DateTime objects serialized that are serialized wherever in an to-be-serialized object in a same specific way, without duplicating logic. (I guess I could put the handling in an abstract class or somewhere similar, yet extending objects also has its pitfalls)
Basically:
Is there a catch an onserialize "event" where I can add some logic, or am I better off looking into JMSSerializer?
I don't know why I didn't submit this as the answer. Since PHP 5.4.0 a JsonSerializable library class is shipped with the PHP install. You can implement this class on your own and create a method named jsonSerialize that will be called whenever json_encode() is called with the class as the argument. A solution to your predicament could be similar to this:
<?php
class Price implements JsonSerializable {
private
$amount
, $currency
, $dueDate
;
/**
* Price constructor.
* #param float $amount
* #param string $currency
* #param \DateTime|null $dueDate
*/
public function __construct($amount = 0.0, $currency = "", $dueDate = NULL)
{
$this->amount = $amount;
$this->currency = $currency;
$this->dueDate = $dueDate;
}
public function jsonSerialize(){
return array(
'amount' => $this->amount
, 'currency' => $this->currency
, 'dueDate' => $this->dueDate instanceof \DateTime ? $this->dueDate->format(\DateTime::ATOM) : $this->dueDate
}
}
echo json_encode(new Price(235, "EUR", new DateTime()));
So you have 3 options:
Use the JsonSerializable interface like #iam-coder proposed.
Use a full-blown serializer like JMS (can be slow).
Use a transformer, the plus side of this is that your output is decoupled from your data, and you can change and test each component on it's own.
Related
When getting an object from an API, I receive a properly serialized Course object.
"startDate": "2018-05-21",
But when I create a new object and try to return it, the formatting is not applied.
"startDate": "2019-02-01T02:37:02+00:00",
Even if I use the repository to get a new Course object, if it is the same object I have just created then it is still not serialized with formatting. Maybe because it is already loaded in memory by that point?
If I use the repository to get a different course from the database then the serialization formatting is applied.
I expected formatting to be applied when I return a Course object regardless of whether it has just been created or not. Any ideas?
Course class
/**
* #ORM\Entity(repositoryClass="App\Repository\CourseRepository")
*
* #HasLifecycleCallbacks
*/
class Course
{
/**
* #var string
*
* #ORM\Column(type="date")
*
* #Assert\Date
* #Assert\NotNull
*
* #JMS\Type("DateTime<'Y-m-d'>")
* #JMS\Groups({"courses-list", "course-details"})
*/
private $startDate;
/**
* #return string
*/
public function getStartDate(): string
{
return $this->startDate;
}
}
Course API Controller Class
public function getCourse($id)
{
$em = $this->getDoctrine()->getManager();
$repo = $em->getRepository('App:Course');
$course = $repo->find($id);
if(!$course) {
throw new NotFoundHttpException('Course not found', null, 2001);
}
return $course;
}
public function addCourse(Request $request) {
$course = new Course();
$course->setStartDate($startDate);
$validator = $this->get('validator');
$em->persist($course);
$em->flush();
return $course;
}
Turns out that you shouldn't use Carbon objects with JMS Serializer.
As soon as I set DateTime objects on the Course object instead of Carbon objects, it worked fine.
Strange behaviour considering that they both implement DateTimeInterface.
I had a discussion with my Team Lead, regarding UnitTest, the question was,
In UnitTest do we use Object Mocking or use the Real Object?
I was supporting the Object Mocking concept, as we should only input/output data from Objects.
At the end we agreed to use Real object instead of Mocking so the following was my Test
<?php
namespace App\Services\Checkout\Module\PaymentMethodRules;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
class AdminRule implements PaymentMethodRule
{
/**
* #var boolean
*/
private $isAdmin;
/**
* #var bool
*/
private $isBankTransferAvailable;
/**
* #param boolean $isAdmin
* #param bool $isBankTransferAvailable
*/
public function __construct($isAdmin, $isBankTransferAvailable)
{
$this->isAdmin = $isAdmin;
$this->isBankTransferAvailable = $isBankTransferAvailable;
}
/**
* #param PaymentMethodRuleManager $paymentMethodRuleManager
*/
public function run(PaymentMethodRuleManager $paymentMethodRuleManager)
{
if ($this->isAdmin) {
$paymentMethodRuleManager->getList()->add([Method::INVOICE]);
}
if ($this->isAdmin && $this->isBankTransferAvailable) {
$paymentMethodRuleManager->getList()->add([Method::BANK_TRANSFER]);
}
}
}
<?php
namespace tests\Services\Checkout\Module;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodList;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
use App\Services\Checkout\Module\PaymentMethodRules\AdminRule;
class AdminRuleTest extends \PHPUnit_Framework_TestCase
{
const IS_ADMIN = true;
const IS_NOT_ADMIN = false;
const IS_BANK_TRANSFER = true;
const IS_NOT_BANK_TRANSFER = false;
/**
* #test
* #dataProvider runDataProvider
*
* #param bool $isAdmin
* #param bool $isBankTransferAvailable
* #param array $expected
*/
public function runApplies($isAdmin, $isBankTransferAvailable, $expected)
{
$paymentMethodRuleManager = new PaymentMethodRuleManager(
new PaymentMethodList([]),
new PaymentMethodList([])
);
$adminRule = new AdminRule($isAdmin, $isBankTransferAvailable);
$adminRule->run($paymentMethodRuleManager);
$this->assertEquals($expected, $paymentMethodRuleManager->getList()->get());
}
/**
* #return array
*/
public function runDataProvider()
{
return [
[self::IS_ADMIN, self::IS_BANK_TRANSFER, [Method::INVOICE, Method::BANK_TRANSFER]],
[self::IS_ADMIN, self::IS_NOT_BANK_TRANSFER, [Method::INVOICE]],
[self::IS_NOT_ADMIN, self::IS_BANK_TRANSFER, []],
[self::IS_NOT_ADMIN, self::IS_NOT_BANK_TRANSFER, []]
];
}
}
My question is, in Unit Test should is use Real Objects or Object Mocking and why?
Second Question, the given Unit test is right or wrong in terms of Unit testing.
The generic answer to such a generic question is: you prefer to use as much of "real" code as possible when doing unit tests. Real code should be default, mocked code is the exception!
But of course, there are various valid reasons to use mocking:
The "real" code does not work in your test setup.
You want to use your mocking framework also to verify that certain actions took place
Example: the code that you intend to test makes a call to some remote service (maybe a database server). Of course that means that you need some tests that do the end to end testing. But for many tests, it might be much more convenient to not do that remote call; instead you would use mocking here - to avoid the remote database call.
Alternatively, as suggested by John Joseph; you might also start with mocking all/most dependencies; to then gradually replace mocking with real calls. This process can help with staying focused on testing exactly "that part" that you actually want to test (instead of getting lost in figuring why your tests using "real other code" is giving you troubles).
IMHO I think it would be good if the original code could be tested directly without any mocking as this would make it less error-prone, and would avoid the debate that if the mocked object behaves almost the same as the original one, but we are not living in the world of unicorns anymore, and mocking is a necessary evil or it is not? This remains the question.
So I think I can rephrase your question to be when to use dummy, fake, stub, or mock?
Generally, the aforementioned terms are known as Test doubles.
As a start, you can check this answer here
Some of the cases when test doubles might be good:
The object under test/System Under Test (SUT) a lot of dependencies, that are required for initialization purposes, and these dependencies would not affect the test, so these dependencies can be dummy ones.
/**
* #inheritdoc
*/
protected function setUp()
{
$this->servicesManager = new ServicesManager(
$this->getDummyEntity()
// ........
);
}
/**
* #return \PHPUnit_Framework_MockObject_MockObject
*/
private function getDummyEntity()
{
return $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods([])
->getMock();
}
SUT has an external dependencies such as an Infrastructure/Resource (e.g. web service, database, cash, file …), then it is a good approach to fake that by using in-memory representation, as one of the reasons to do that is to avoid cluttering this Infrastructure/Resource with test data.
/**
* #var ArrayCollection
*/
private $inMemoryRedisDataStore;
/**
* #var DataStoreInterface
*/
private $fakeDataStore;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->inMemoryRedisDataStore = new Collections\ArrayCollection;
$this->fakeDataStore = $this->getFakeRedisDataStore();
$this->sessionHandler = new SessionHanlder($this->fakeDataStore);
}
/**
* #return \PHPUnit_Framework_MockObject_MockObject
*/
private function getFakeRedisDataStore()
{
$fakeRedis = $this->getMockBuilder(
Infrastructure\Memory\Redis::class
)
->disableOriginalConstructor()
->setMethods(['set', 'get'])
->getMock();
$inMemoryRedisDataStore = $this->inMemoryRedisDataStore;
$fakeRedis->method('set')
->will(
$this->returnCallback(
function($key, $data) use ($inMemoryRedisDataStore) {
$inMemoryRedisDataStore[$key] = $data;
}
)
);
$fakeRedis->method('get')
->will(
$this->returnCallback(
function($key) use ($inMemoryRedisDataStore) {
return $inMemoryRedisDataStore[$key];
}
)
);
}
When there is a need of asserting the state of SUT, then stubs become handy. Usually, this would be confused with a fake object, and to clear this out, fake objects are helping objects and they should never be asserted.
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
public function isSent(): bool;
}
/**
* Class SMSProviderStub
*/
class SMSProviderStub implements Provider\SMSProviderInterface
{
/**
* #var bool
*/
private $isSent;
/**
* #inheritdoc
*/
public function send()
{
$this->isSent = true;
}
/**
* #return bool
*/
public function isSent(): bool
{
return $this->isSent;
}
}
/**
* Class PaymentServiceTest
*/
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Service\PaymentService
*/
private $paymentService;
/**
* #var SMSProviderInterface
*/
private $smsProviderStub;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->smsProviderStub = $this->getSMSProviderStub();
$this->paymentService = new Service\PaymentService(
$this->smsProviderStub
);
}
/**
* Checks if the SMS was sent after payment using stub
* (by checking status).
*
* #param float $amount
* #param bool $expected
*
* #dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount, bool $expected)
{
$this->paymentService->pay($amount);
$this->assertEquals($expected, $this->smsProviderStub->isSent());
}
/**
* #return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should return true' => [
'amount' => 28.99,
'expected' => true,
],
];
}
/**
* #return Provider\SMSProviderInterface
*/
private function getSMSProviderStub(): Provider\SMSProviderInterface
{
return new SMSProviderStub();
}
}
If the behavior of SUT should be checked then mocks most probably will come to the rescue or stubs (Test spy), it can be detected as simple as that most probably no assert statements should be found. for example, the mock can be setup to behave like when it get a call to X method with values a, and b return the value Y or expect a method to be called once or N of times, ..etc.
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
}
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Service\PaymentService
*/
private $paymentService;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->paymentService = new Service\PaymentService(
$this->getSMSProviderMock()
);
}
/**
* Checks if the SMS was sent after payment using mock
* (by checking behavior).
*
* #param float $amount
*
* #dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount)
{
$this->paymentService->pay($amount);
}
/**
* #return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should check behavior' => [
'amount' => 28.99,
],
];
}
/**
* #return SMSProviderInterface
*/
private function getSMSProviderMock(): SMSProviderInterface
{
$smsProviderMock = $this->getMockBuilder(Provider\SMSProvider::class)
->disableOriginalConstructor()
->setMethods(['send'])
->getMock();
$smsProviderMock->expects($this->once())
->method('send')
->with($this->anything());
}
}
Corner cases
SUT has a lot of dependencies which are dependent on other things, and to avoid this dependency loop as we are only interested in testing some methods, the whole object can be mocked, but with having the ability to forward the calls to the original methods.
$testDouble = $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods(null);
As per Ahmed Kamal's answer, it worked as expected.
I tested the below sample.
Foo.php
<?php
class Foo
{
/**
* Tell Foo class Name
* #param string $name
* #return string
*/
public function tellName(string $name = 'Josh'): string
{
return 'Hi ' . $name;
}
}
FooTest.php
<?php
include('Foo.php');
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
/**
* PHPUnit testing with assertEquals
* #return void
*/
public function testTellName()
{
// create the class object
$mockObj = $this->getMockBuilder(Foo::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
// get the object function result by passing the method parameter value
// pass different parameter value to get an invalid result
$result = $mockObj->tellName('John');
// validate the result with assertEquals()
$this->assertEquals('Hi John', $result);
}
}
Error and Success results:
Cheers!
My consumed XML API has an option to retrieve only parts of the response.
This causes the resulting object to have a lot of NULL properties if this feature is used.
Is there a way to actually skip NULL properties? I tried to implement an exclusion strategy with
shouldSkipProperty(PropertyMetadata $property, Context $context)`
but i realized there is no way to access the current property value.
An example would be the following class
class Hotel {
/**
* #Type("string")
*/
public $id;
/**
* #Type("integer")
*/
public $bookable;
/**
* #Type("string")
*/
public $name;
/**
* #Type("integer")
*/
public $type;
/**
* #Type("double")
*/
public $stars;
/**
* #Type("MssPhp\Schema\Response\Address")
*/
public $address;
/**
* #Type("integer")
*/
public $themes;
/**
* #Type("integer")
*/
public $features;
/**
* #Type("MssPhp\Schema\Response\Location")
*/
public $location;
/**
* #Type("MssPhp\Schema\Response\Pos")
*/
public $pos;
/**
* #Type("integer")
*/
public $price_engine;
/**
* #Type("string")
*/
public $language;
/**
* #Type("integer")
*/
public $price_from;
}
which deserializes in this specific api call to the following object with a lot of null properties.
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"address": null,
"themes": null,
"features": null,
"location": null,
"pos": null,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
But i want it to be
"hotel": [
{
"id": "11230",
"bookable": 1,
"name": "Hotel Test",
"type": 1,
"stars": 3,
"price_engine": 0,
"language": "de",
"price_from": 56
}
]
You can configure JMS Serializer to skip null properties like so:
$serializer = JMS\SerializerBuilder::create();
$serializedString = $serializer->serialize(
$data,
'xml',
JMS\SerializationContext::create()->setSerializeNull(true)
);
Taken from this issue.
UPDATE:
Unfortunately, if you don't want the empty properties when deserializing, there is no other way then removing them yourself.
However, I'm not sure what your use case for actually wanting to remove these properties is, but it doesn't look like the Hotel class contains much logic. In this case, I'm wondering whether the result has should be a class at all ?
I think it would be more natural to have the data represented as an associative array instead of an object. Of course, JMS Serializer cannot deserialize your data into an array, so you will need a data transfer object.
It's enough that you add dumpArray and loadArray methods to your existing Hotel class. These will be used for transforming the data into your desired result and vice versa. There is your DTO.
/**
* Sets the object's properties based on the passed array
*/
public function loadArray(array $data)
{
}
/**
* Returns an associative array based on the objects properties
*/
public function dumpArray()
{
// filter out the properties that are empty here
}
I believe it's the cleanest approach and it might reflect what you're trying to do more.
I hope this helps.
I am trying to figure out if it is possible to use PHPdoc to define the object properties being returned by a function or a object method.
Say I have the following class:
class SomeClass {
public function staffDetails($id){
$object = new stdClass();
$object->type = "person";
$object->name = "dave";
$object->age = "46";
return $object;
}
}
Now, it is easy enough to define input parameters.
/**
* Get Staff Member Details
*
* #param string $id staff id number
*
* #return object
*/
class SomeClass {
public function staffDetails($id){
$object = new stdClass();
$object->type = "person";
$object->name = "dave";
$object->age = "46";
return $object;
}
}
The question is is there a similar thing for defining properties of the output object (of a stdClass) returned by the method in question. So that another programmer does not have to open this class and manually look into the method to see what the return object is returning?
Here it is 4 years later, and there still does not appear to be a way to annotate the properties of a stdClass object as originally described in your question.
Collections had been proposed in PSR-5, but that appears to have been shot down: https://github.com/php-fig/fig-standards/blob/211063eed7f4d9b4514b728d7b1810d9b3379dd1/proposed/phpdoc.md#collections
It seems there are only two options available:
Option 1:
Create a normal class representing your data object and annotate the properties.
class MyData
{
/**
* This is the name attribute.
* #var string
*/
public $name;
/**
* This is the age attribute.
* #var integer
*/
public $age;
}
Option 2:
Create a generic Struct type class as suggested by Gordon and extend it as your data object, using the #property annotation to define what generic values are possible to access with __get and __set.
class Struct
{
/**
* Private internal struct attributes
* #var array
*/
private $attributes = [];
/**
* Set a value
* #param string $key
* #param mixed $value
*/
public function __set($key, $value)
{
$this->attributes[$key] = $value;
}
/**
* Get a value
* #param string $key
* #return mixed
*/
public function __get($key)
{
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
}
/**
* Check if a key is set
* #param string $key
* #return boolean
*/
public function __isset($key)
{
return isset($this->attributes[$key]) ? true : false;
}
}
/**
* #property string $name
* #property integer $age
*/
class MyData extends Struct
{
// Can optionally add data mutators or utility methods here
}
You have only two way to document the structure of the result class.
1.One can describe the structure in a comment text. For example:
class SomeClass
{
/**
* Getting staff detail.
* Result object has following structure:
* <code>
* $type - person type
* $name - person name
* $age - person age
* </code>
* #param string $id staff id number
*
* #return stdClass
*
*/
public function staffDetails($id){
$object = new stdClass();
$object->type = "person";
$object->name = "dave";
$object->age = "46";
return $object;
}
}
2.One can create a data type that will inheritance stdClass and it will have an annotation of a result object. For example:
/**
* #property string $type Person type
* #property string $name Person name
* #property integer $age Person age
*/
class DTO extends stdClass
{}
And use it in your other classes
class SomeClass {
/**
* Getting staff detail.
*
* #param string $id staff id number
*
* #return DTO
*
*/
public function staffDetails($id){
$object = new DTO();
$object->type = "person";
$object->name = "dave";
$object->age = "46";
return $object;
}
}
In my opinion, this way is better than a description in the text comment because it makes the code more obvious
If you are using PHP 7, you can define anonymous class.
class SomeClass {
public function staffDetails($id){
$object = (new class() extends stdClass {
public /** #var string */ $type;
public /** #var string */ $name;
public /** #var int */ $age;
});
$object->type = "person";
$object->name = "dave";
$object->age = 46;
return $object;
}
}
It is working for my IDE (tested in NetBeans)
With for example json_decode it's harder to use own classes instead of stdClass, but in my case I just created dummy file with class definitions, which really isn't loaded and I'm adding own classes as #return (works for intelephense on vscode).
PHPdocObjects.php
/**
* class only for PHPdoc (do not include)
*/
class Member {
/** #var string */
public $type;
/** #var string */
public $name;
/** #var string */
public $age;
}
/**
* Other format
*
* #property string $type;
* #property string $name;
* #property string $age;
*/
class MemberAlt {}
SomeClass.php
/**
* Get Staff Member Details
*
* #param string $id staff id number
*
* #return Member I'm in fact stdClass
*/
class SomeClass {
public function staffDetails($id){
$object = json_decode('{"type":"person","name":"dave","age":"46"}');
return $object;
}
}
The hack I use for autocomplete in PhpStorm:
Create some meta file which will contain some classes to describe your structures. The file is never included and structures have their own name rules in order not to mess them with real existing classes:
<?php
/*
meta.php
never included
*/
/**
* #property string $type
* #property string $name
* #property string $age
*/
class StaffDetails_meta {}
Use the meta class as a return value in your real code PHPDoc:
<?php
/*
SomeClass.php
eventually included
*/
class SomeClass
{
/**
* Get Staff Member Details
*
* #param string $id staff id number
*
* #return StaffDetails_meta
*/
public function staffDetails($id)
{
$object = new stdClass();
$object->type = "person";
$object->name = "dave";
$object->age = "46";
return $object;
}
}
Congratulations, this will make your IDE autocomplete your code when you'd typing something like (new SomeClass)->staffDetails('staff_id')->
P.S.: I know, almost 10 years passed but still actual
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.