Nelmio Api Doc Bundle: Documentating required Parameters - php

I am currently working with the NelmioApiDocBundle, with which I am not very familiar yet. The API I am writing has to provide a route to change the password of a specific user. The documentation should state, that for changing the password both the old one and the new one are required. Since I did not found an explanation of the difference between Requirementsand Parameters, I guess the first is used for data from the route and the latter is used for the API call itself.
First attempt of archieving such a documentation was to implement a simple Model, which the JMSSerializerBundle then automatically converts:
class ChangePasswordParam
{
/**
* #Type("string")
* #var string
*/
protected $oldPassword;
/**
* #Type("string")
* #var string
*/
protected $newPassword;
}
The Controller accepts the API call via this action method:
/**
* Changes the password for a specific user.
*
* #Post("/{username}/changepassword")
* #View()
* #ApiDoc(
* description="Changes the password of a User",
* input="FQCN\ChangePasswordParam"
* )
*
* #param string $username
* #param ChangePasswordParam $passwordParam
*
* #return Response
*/
public function changePasswordAction($username, ChangePasswordParam $passwordParam)
{
/* ... */
}
This led to the documentation showing username as Requirement, old_password and new_password as Parameter. To mark those Parameters as required, I added a Symfony Constraint via annotation to the properties:
class ChangePasswordParam
{
/**
* #Type("string")
* #Assert\NotNull()
* #var string
*/
protected $oldPassword;
/**
* #Type("string")
* #Assert\NotNull()
* #var string
*/
protected $newPassword;
}
However, while using these annotations marked the properties as required, it does generate strange output:
Notice the parameters being added twice and in different formats? Adding the #SerializedName("old_password") has no effect. Regarding this ticket, the issue is still not solved.
Another way of accepting data for the action is using a custom form, which indeed marks the properties as required but also generates no proper output. Changing the ChangePasswordParam as custom form:
class ChangePasswordParam extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('old_password', 'text');
$builder->add('new_password', 'text');
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'change_password';
}
}
is resulting in this parameter description:
Those parameters should be named just 'old_password' and 'new_password' and I can't figure out how to archieve this.
Thanks in advance

Your #ApiDoc annotation should include an empty input form name field like below:
* #ApiDoc(
* description="Changes the password of a User",
* input= {
* "class" = "FQCN\ChangePasswordParam",
* "name" = ""
* }
* )
This will remove the form name before the parameters name.

Related

Add extra option to LoggableListener

I use Loggable to backup changes in Entities.
The default AbstractLogEntry does not have enough columns for my needs.
Thats why i extended the class and added extra getters and setters.
See the code below
/**
* EmployeeBackup
*
* #ORM\Table(name="employee_backup")
* #ORM\Entity(repositoryClass="Gedmo\Loggable\Entity\Repository\LogEntryRepository")
*
*/
class EmployeeBackup extends AbstractLogEntry
{
/**
* #var int
*
* #ORM\Column(name="division_id", type="integer", unique=true)
*/
private $divisionId;
/**
* #return int
*/
public function getDivisionId(): int
{
return $this->divisionId;
}
/**
* #param string $divisionId
*/
public function setDivisionId(string $divisionId): void
{
$this->divisionId = $divisionId;
}
}
The extension is using the class above. So it works.
But now i need to set the divisionId when a new version is stored.
I tried the code below
$loggable = new LoggableListener();
$loggable->setDivision($division);
$evm->addEventSubscriber($loggable);
And this is what i get:
Attempted to call an undefined method named "setDivision" of class "Gedmo\Loggable\LoggableListener".
And thats true because LoggableListener does not have a setDivision function. My question is: Do i need to override the listener and if so, how do i do that?
Thanks ;)

api-platform | Is there a way to disable the WriteListener on a specific operation or route?

So because of api-platform.com Unable to generate an IRI for the item of type I tried using a different approach and declare custom operations on my user entity for login, registration and reset (since I stil want custom business logics for them). So the initial set-up of that in api-platform is rather easy. I added the following code to my user entity
* collectionOperations={
* "register"={"route_name"="user_register","normalization_context"={"groups"={"registerRead"}},"denormalization_context"={"groups"={"registerWrite"}}},
* "reset"={"route_name"="user_reset","normalization_context"={"groups"={"resetRead"}},"denormalization_context"={"groups"={"resetWrite"}}},
* "login"={"route_name"="user_login","normalization_context"={"groups"={"loginRead"}},"denormalization_context"={"groups"={"loginWrite"}}},
* "token"={"route_name"="user_token","normalization_context"={"groups"={"tokenRead"}},"denormalization_context"={"groups"={"token"}}}
* },
And then added the appropriate actions to the user controller.
/**
* #Route(
* name="user_login",
* path="api/user/login",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="login",
* "_api_receive"=false
* }
* )
*/
public function loginAction(User $data): User {
///$this->userService->login($data);
return $data;
}
/**
* #Route(
* name="user_register",
* path="api/user/register",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="register",
* "_api_receive"=false
* }
* )
*/
public function registerAction(User $data): User {
///$this->userService->register($data);
return $data;
}
/**
* #Route(
* name="user_reset",
* path="api/user/reset",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="reset",
* "_api_receive"=false
* }
* )
*/
public function resetAction(User $data): User {
//$this->userService->reset($data);
return $data;
}
/**
* #Route(
* name="user_token",
* path="api/user/token",
* methods={"POST"},
* defaults={
* "_api_resource_class"=User::class,
* "_api_collection_operation_name"="token",
* "_api_receive"=false
* }
* )
*/
public function tokenAction(User $data): User {
//$this->userService->reset($data);
return $data;
}
So far al fine, however..... because we are using a post operation here and the user is a doctrine ORM entity the api-platform bundle atomically adds the post to the database. But I don’t want that, I want it to pass the entity on to the controller who then uses a service to do business logics. And determine if and how the post should be processed.
Now I went over the documentation and the problem seems to be that the WriteListener always triggers there were other triggers (e.g. ReadListener, DeserializeListener and ValidateListener) can be disabled trough the _api_receive parameter.
So that leaves the question is there a way to disable the WriteListener on a specific operation or route?
Kind Regards,
Ruben van der Linde
You can return an instance of HttpFoundation's Response instead of $data. Then no listener registered on kernel.view will be called.
But introducing a listener similar to api_receive for the write listener is a good idea. Would you mind opening a Pull Request?
Edit: I've opened a Pull Request to introduce this new flag: https://github.com/api-platform/core/pull/2072

Failed to set up a ManyToMany

I want to be able to select a school (that has its own entity) while creating a mission (also has its entity)
Since a school can have several missions, and you can select several schools at the mission's creation, I used a ManyToMany.
The problem is that after creating this "ManyToMany", generating the entities and updating my schema, Symfony created a table, but left it totally empty, without the two columns that I asked for. I'm not really used to Symfony nor to the ManyToMany system, so I might have done some mistake without noticing it, still I find this weird.
Here's the interesting part of my ecole (school) entity:
class Ecole{
// ...
/**
* #ORM\ManyToMany(targetEntity="MissionBundle\Entity\Mission", mappedBy="ecolesDispo")
*/
protected $missionsDispos;
// ...
/**
* Add missionsDispo
*
* #param \MissionBundle\Entity\Mission $missionsDispo
*
* #return Ecole
*/
public function addMissionsDispo(\MissionBundle\Entity\Mission $missionsDispo)
{
$this->missionsDispos[] = $missionsDispo;
return $this;
}
/**
* Remove missionsDispo
*
* #param \MissionBundle\Entity\Mission $missionsDispo
*/
public function removeMissionsDispo(\MissionBundle\Entity\Mission $missionsDispo)
{
$this->missionsDispos->removeElement($missionsDispo);
}
/**
* Get missionsDispos
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMissionsDispos()
{
return $this->missionsDispos;
}
And here is the interesting part of my mission entity:
/**
* #ORM\ManyToMany(targetEntity="EcoleBundle\Entity\Ecole", inversedBy="missionsDispo")
* #ORM\JoinTable(name="Mission2Ecole",
* joinColumns={#ORM\JoinColumn(name="em_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="me_id", referencedColumnName="id")}
* )
*/
protected $ecolesDispo;
// ...
/**
* Constructor
*/
public function __construct()
{
$this->ecolesDispo = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add ecolesDispo
*
* #param \EcoleBundle\Entity\Ecole $ecolesDispo
*
* #return Mission
*/
public function addEcolesDispo(\EcoleBundle\Entity\Ecole $ecolesDispo)
{
$this->ecolesDispo[] = $ecolesDispo;
return $this;
}
/**
* Remove ecolesDispo
*
* #param \EcoleBundle\Entity\Ecole $ecolesDispo
*/
public function removeEcolesDispo(\EcoleBundle\Entity\Ecole $ecolesDispo)
{
$this->ecolesDispo->removeElement($ecolesDispo);
}
/**
* Get ecolesDispo
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getEcolesDispo()
{
return $this->ecolesDispo;
}
After all this was created, I was supposed to get a multi selector with the list of all the schools saved in the database (I already added it to the missionType file), but I get absolutely nothing.
I don't really know if I inverted the annotations, or if the "joinTable" part is correct, but I'm completely lost here.
Does anyone have an idea?
Thank you in advance
Just wrong typo "s"? inversedBy="missionsDispo" >>> inversedBy="missionsDispos"
PS. Official doc here
http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/association-mapping.html#many-to-many-bidirectional

TYPO3 6.2 Extbase: Persisted Object still Modified

this may be a Simple issue and im just overseeing it.
What I wanna do:
I have a Model and I simply create an instance of it, fill it with Data and then use the add($object) function of my Repository.
Then I Call the persistenceManager to persist my Entry.
The whole Controller is part of an Backend Module.
I have to Persist the Entry Manualy, because there is a #header Redirect, because of an SSO API Call which redirects me afterwards back.
What is the Problem:
My Debugs hint that after adding Values to the Object the Propertys filled to the Object correctly.
After calling the persistAll function, my Object is Persisted, but the Values appear as "modified".
The result is:
I have an Empty Object in my Table.
Time for some Code (shortened)
/**
* myObjectRepository
*
* #var \MyVendor\MyExt\Domain\Repository\MyObjectRepository
* #inject
*/
protected $myObjectRepository;
/**
* #var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
* #inject
*/
protected $objectManager;
/**
* #var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
* #inject
*/
protected $persistenceManager;
/**
* action verify
*
* #return void
*/
public function verifyAction() {
$myObject = $this->objectManager->get('\\MyVendor\\MyExt\\Domain\\Model\\MyObject');
$myObject->setName('Nice Name');
$myObject->setAge(20);
$this->myObjectRepository->add($myObject);
$this->persistenceManager->persistAll();
}
This is to give an Idea of what I try to do.
I'm kinda stunned with this issue. The normal Behaviour should be a Persisted Database Entry. Maybe its just a Simple detail overseen.
same problem here! Debugging shows, that the persistObject-function called the $dataMap->isPersistableProperty($propertyName) function before pass the data to DB.
this function needs an TCA
/**
* Returns TRUE if the property is persistable (configured in $TCA)
*
* #param string $propertyName The property name
* #return boolean TRUE if the property is persistable (configured in $TCA)
*/
public function isPersistableProperty($propertyName) {
return isset($this->columnMaps[$propertyName]);
}
so, be sure your TCA isset and correct to pass the properties to the DB

Zend Framework and Doctrine 2 - are my unit tests sufficient?

I'm quite new to Zend and unit testing in general. I have come up with a small application that uses Zend Framework 2 and Doctrine. It has only one model and controller and I want to run some unit tests on them.
Here's what I have so far:
Base doctrine 'entity' class, containing methods I want to use in all of my entities:
<?php
/**
* Base entity class containing some functionality that will be used by all
* entities
*/
namespace Perceptive\Database;
use Zend\Validator\ValidatorChain;
class Entity{
//An array of validators for various fields in this entity
protected $validators;
/**
* Returns the properties of this object as an array for ease of use. Will
* return only properties with the ORM\Column annotation as this way we know
* for sure that it is a column with data associated, and won't pick up any
* other properties.
* #return array
*/
public function toArray(){
//Create an annotation reader so we can read annotations
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
//Create a reflection class and retrieve the properties
$reflClass = new \ReflectionClass($this);
$properties = $reflClass->getProperties();
//Create an array in which to store the data
$array = array();
//Loop through each property. Get the annotations for each property
//and add to the array to return, ONLY if it contains an ORM\Column
//annotation.
foreach($properties as $property){
$annotations = $reader->getPropertyAnnotations($property);
foreach($annotations as $annotation){
if($annotation instanceof \Doctrine\ORM\Mapping\Column){
$array[$property->name] = $this->{$property->name};
}
}
}
//Finally, return the data array to the user
return $array;
}
/**
* Updates all of the values in this entity from an array. If any property
* does not exist a ReflectionException will be thrown.
* #param array $data
* #return \Perceptive\Database\Entity
*/
public function fromArray($data){
//Create an annotation reader so we can read annotations
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
//Create a reflection class and retrieve the properties
$reflClass = new \ReflectionClass($this);
//Loop through each element in the supplied array
foreach($data as $key=>$value){
//Attempt to get at the property - if the property doesn't exist an
//exception will be thrown here.
$property = $reflClass->getProperty($key);
//Access the property's annotations
$annotations = $reader->getPropertyAnnotations($property);
//Loop through all annotations to see if this is actually a valid column
//to update.
$isColumn = false;
foreach($annotations as $annotation){
if($annotation instanceof \Doctrine\ORM\Mapping\Column){
$isColumn = true;
}
}
//If it is a column then update it using it's setter function. Otherwise,
//throw an exception.
if($isColumn===true){
$func = 'set'.ucfirst($property->getName());
$this->$func($data[$property->getName()]);
}else{
throw new \Exception('You cannot update the value of a non-column using fromArray.');
}
}
//return this object to facilitate a 'fluent' interface.
return $this;
}
/**
* Validates a field against an array of validators. Returns true if the value is
* valid or an error string if not.
* #param string $fieldName The name of the field to validate. This is only used when constructing the error string
* #param mixed $value
* #param array $validators
* #return boolean|string
*/
protected function setField($fieldName, $value){
//Create a validator chain
$validatorChain = new ValidatorChain();
$validators = $this->getValidators();
//Try to retrieve the validators for this field
if(array_key_exists($fieldName, $this->validators)){
$validators = $this->validators[$fieldName];
}else{
$validators = array();
}
//Add all validators to the chain
foreach($validators as $validator){
$validatorChain->attach($validator);
}
//Check if the value is valid according to the validators. Return true if so,
//or an error string if not.
if($validatorChain->isValid($value)){
$this->{$fieldName} = $value;
return $this;
}else{
$err = 'The '.$fieldName.' field was not valid: '.implode(',',$validatorChain->getMessages());
throw new \Exception($err);
}
}
}
My 'config' entity, which represents a one-row table containing some configuration options:
<?php
/**
* #todo: add a base entity class which handles validation via annotations
* and includes toArray function. Also needs to get/set using __get and __set
* magic methods. Potentially add a fromArray method?
*/
namespace Application\Entity;
use Doctrine\ORM\Mapping as ORM;
use Zend\Validator;
use Zend\I18n\Validator as I18nValidator;
use Perceptive\Database\Entity;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks
*/
class Config extends Entity{
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserId;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserName;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $minLengthUserPassword;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $daysPasswordReuse;
/**
* #ORM\Id
* #ORM\Column(type="boolean")
*/
protected $passwordLettersAndNumbers;
/**
* #ORM\Id
* #ORM\Column(type="boolean")
*/
protected $passwordUpperLower;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $maxFailedLogins;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $passwordValidity;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $passwordExpiryDays;
/**
* #ORM\Id
* #ORM\Column(type="integer")
*/
protected $timeout;
// getters/setters
/**
* Get the minimum length of the user ID
* #return int
*/
public function getMinLengthUserId(){
return $this->minLengthUserId;
}
/**
* Set the minmum length of the user ID
* #param int $minLengthUserId
* #return \Application\Entity\Config This object
*/
public function setMinLengthUserId($minLengthUserId){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserId', $minLengthUserId);
}
/**
* Get the minimum length of the user name
* #return int
*/
public function getminLengthUserName(){
return $this->minLengthUserName;
}
/**
* Set the minimum length of the user name
* #param int $minLengthUserName
* #return \Application\Entity\Config
*/
public function setMinLengthUserName($minLengthUserName){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserName', $minLengthUserName);
}
/**
* Get the minimum length of the user password
* #return int
*/
public function getMinLengthUserPassword(){
return $this->minLengthUserPassword;
}
/**
* Set the minimum length of the user password
* #param int $minLengthUserPassword
* #return \Application\Entity\Config
*/
public function setMinLengthUserPassword($minLengthUserPassword){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('minLengthUserPassword', $minLengthUserPassword);
}
/**
* Get the number of days before passwords can be reused
* #return int
*/
public function getDaysPasswordReuse(){
return $this->daysPasswordReuse;
}
/**
* Set the number of days before passwords can be reused
* #param int $daysPasswordReuse
* #return \Application\Entity\Config
*/
public function setDaysPasswordReuse($daysPasswordReuse){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('daysPasswordReuse', $daysPasswordReuse);
}
/**
* Get whether the passwords must contain letters and numbers
* #return boolean
*/
public function getPasswordLettersAndNumbers(){
return $this->passwordLettersAndNumbers;
}
/**
* Set whether passwords must contain letters and numbers
* #param int $passwordLettersAndNumbers
* #return \Application\Entity\Config
*/
public function setPasswordLettersAndNumbers($passwordLettersAndNumbers){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordLettersAndNumbers', $passwordLettersAndNumbers);
}
/**
* Get whether password must contain upper and lower case characters
* #return type
*/
public function getPasswordUpperLower(){
return $this->passwordUpperLower;
}
/**
* Set whether password must contain upper and lower case characters
* #param type $passwordUpperLower
* #return \Application\Entity\Config
*/
public function setPasswordUpperLower($passwordUpperLower){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordUpperLower', $passwordUpperLower);
}
/**
* Get the number of failed logins before user is locked out
* #return int
*/
public function getMaxFailedLogins(){
return $this->maxFailedLogins;
}
/**
* Set the number of failed logins before user is locked out
* #param int $maxFailedLogins
* #return \Application\Entity\Config
*/
public function setMaxFailedLogins($maxFailedLogins){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('maxFailedLogins', $maxFailedLogins);
}
/**
* Get the password validity period in days
* #return int
*/
public function getPasswordValidity(){
return $this->passwordValidity;
}
/**
* Set the password validity in days
* #param int $passwordValidity
* #return \Application\Entity\Config
*/
public function setPasswordValidity($passwordValidity){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordValidity', $passwordValidity);
}
/**
* Get the number of days prior to expiry that the user starts getting
* warning messages
* #return int
*/
public function getPasswordExpiryDays(){
return $this->passwordExpiryDays;
}
/**
* Get the number of days prior to expiry that the user starts getting
* warning messages
* #param int $passwordExpiryDays
* #return \Application\Entity\Config
*/
public function setPasswordExpiryDays($passwordExpiryDays){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('passwordExpiryDays', $passwordExpiryDays);
}
/**
* Get the timeout period of the application
* #return int
*/
public function getTimeout(){
return $this->timeout;
}
/**
* Get the timeout period of the application
* #param int $timeout
* #return \Application\Entity\Config
*/
public function setTimeout($timeout){
//Use the setField function, which checks whether the field is valid,
//to set the value.
return $this->setField('timeout', $timeout);
}
/**
* Returns a list of validators for each column. These validators are checked
* in the class' setField method, which is inherited from the Perceptive\Database\Entity class
* #return array
*/
public function getValidators(){
//If the validators array hasn't been initialised, initialise it
if(!isset($this->validators)){
$validators = array(
'minLengthUserId' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'minLengthUserName' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(2),
),
'minLengthUserPassword' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(3),
),
'daysPasswordReuse' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
),
'passwordLettersAndNumbers' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
new Validator\LessThan(2),
),
'passwordUpperLower' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(-1),
new Validator\LessThan(2),
),
'maxFailedLogins' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(0),
),
'passwordValidity' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'passwordExpiryDays' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(1),
),
'timeout' => array(
new I18nValidator\Int(),
new Validator\GreaterThan(0),
)
);
$this->validators = $validators;
}
//Return the list of validators
return $this->validators;
}
/**
* #todo: add a lifecyle event which validates before persisting the entity.
* This way there is no chance of invalid values being saved to the database.
* This should probably be implemented in the parent class so all entities know
* to validate.
*/
}
And my controller, which can read from and write to the entity:
<?php
/**
* A restful controller that retrieves and updates configuration information
*/
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractRestfulController;
use Zend\View\Model\JsonModel;
class ConfigController extends AbstractRestfulController
{
/**
* The doctrine EntityManager for use with database operations
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* Constructor function manages dependencies
* #param \Doctrine\ORM\EntityManager $em
*/
public function __construct(\Doctrine\ORM\EntityManager $em){
$this->em = $em;
}
/**
* Retrieves the configuration from the database
*/
public function getList(){
//locate the doctrine entity manager
$em = $this->em;
//there should only ever be one row in the configuration table, so I use findAll
$config = $em->getRepository("\Application\Entity\Config")->findAll();
//return a JsonModel to the user. I use my toArray function to convert the doctrine
//entity into an array - the JsonModel can't handle a doctrine entity itself.
return new JsonModel(array(
'data' => $config[0]->toArray(),
));
}
/**
* Updates the configuration
*/
public function replaceList($data){
//locate the doctrine entity manager
$em = $this->em;
//there should only ever be one row in the configuration table, so I use findAll
$config = $em->getRepository("\Application\Entity\Config")->findAll();
//use the entity's fromArray function to update the data
$config[0]->fromArray($data);
//save the entity to the database
$em->persist($config[0]);
$em->flush();
//return a JsonModel to the user. I use my toArray function to convert the doctrine
//entity into an array - the JsonModel can't handle a doctrine entity itself.
return new JsonModel(array(
'data' => $config[0]->toArray(),
));
}
}
Because of character limits on I was unable to paste in my unit tests, but here are links to my unit tests so far:
For the entity:
https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Entity/ConfigTest.php
For the controller:
https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Controller/ConfigControllerTest.php
Some questions:
Am I doing anything obviously wrong here?
In the tests for the entity, I am repeating the same tests for many different fields - is there a way to minimise this? Like have a standard battery of tests to run on integer columns for instance?
In the controller I am trying to 'mock up' doctrine's entity manager so that changes aren't really saved into the database - am I doing this properly?
Is there anything else in the controller which I should test?
Thanks in advance!
While your code appears to be solid enough, it presents a couple of design oversights.
First of all, Doctrine advise to treat entities like simple, dumb value objects, and states that the data they hold is always assumed to be valid.
This means that any business logic, like hydration, filtering and validation, should be moved outside entities to a separate layer.
Speaking of hydration, rather than implementing by yourself fromArray and toArray methods, you could use the supplied DoctrineModule\Stdlib\Hydrator\DoctrineObject hydrator, which can also blend flawlessly with Zend\InputFilter, to handle filtering and validation. This would make entity testing much much less verbose, and arguably not so needed, since you would test the filter separately.
Another important suggestion coming from Doctrine devs is to not inject an ObjectManager directly inside controllers. This is for encapsulation purposes: it is desirable to hide implementation details of your persistence layer to the Controller and, again, expose only an intermediate layer.
In your case, all this could be done by having a ConfigService class, designed by contract, which will only provide the methods you really need (i.e. findAll(), persist() and other handy proxies), and will hide the dependencies that are not strictly needed by the controller, like the EntityManager, input filters and the like. It will also contribute to easier mocking.
This way, if one day you would want to do some changes in your persistence layer, you would just have to change how your entity service implements its contract: think about adding a custom cache adapter, or using Doctrine's ODM rather than the ORM, or even not using Doctrine at all.
Other than that, your unit testing approach looks fine.
TL;DR
You should not embed business logic inside Doctrine entities.
You should use hydrators with input filters together.
You should not inject the EntityManager inside controllers.
An intermediate layer would help implementing these variations, preserving at the same time Model and Controller decoupling.
Your tests look very similar to ours, so there's nothing immediately obvious that you are doing incorrectly. :)
I agree that this "smells" a bit weird, but I don't have an answer for you on this one. Our standard is to make all of our models "dumb" and we do not test them. This is not something I recommend, but because I havent encountered your scenario before I don't want to just guess.
You seem to be testing pretty exhaustively, although I would really recommend checking out the mocking framework: Phake (http://phake.digitalsandwich.com/docs/html/) It really helps to seperate your assertions from your mocking, as well as provides a much more digestable syntax than the built in phpunit mocks.
good luck!

Categories