Symofny2 Get a listing of available Logical Controller Names - php

I need to show a choice with a list of all available controllers as Logical Controller Names AcmeBundle:ControllerName:ActionName
I see that CLI command php app/console router:debug dumps a similar listing, but with controller names, e.g. fos_user_security_login.
How can I ask Symfony for their Logical Controller Name representation?
Thanks!

As #hous said, this post was useful, but incomplete and its accepted answer misleading.
A) Getting the controllers
With this code I get all controllers, but with their FQCN::method or service:method notation.
// in a Controller.
$this->container->get('router')->getRouteCollection()->all()
Some Background
The previous method will return a big array of routes. Follows one key => value:
'admin_chacra' => // route name
object(Symfony\Component\Routing\Route)[1313]
...
private 'defaults' =>
array (size=1)
'_controller' => string 'Application\ColonizacionBundle\Controller\ChacraController::indexAction' (length=71)
The FQCN::method notation is the right argument to the build method of ControllerNameParser::build(). The service notation is not parsed, as it gets handled by the following code in ControllerResolver::createController()`
$count = substr_count($controller, ':');
if (2 == $count) {
// controller in the a:b:c notation then
/* #var $this->parser ControllerNameParser parse() is the oposite of build()*/
$controller = $this->parser->parse($controller);
} elseif (1 == $count) {
// controller in the service:method notation
list($service, $method) = explode(':', $controller, 2);
return array($this->container->get($service), $method);
} else {
throw new \LogicException(sprintf('Unable to parse the controller name "%s".', $controller));
}
B) Generating Logical Controller Names
So all I have to do is filter out the controllers I don't want, {FOS; framework's; etc} and feed build() with each selected one. E.g. by selecting only the _controller attributes that matches my bundles namespace Application\*Bundle in my case.
Here's the build docBlock
/**
* Converts a class::method notation to a short one (a:b:c).
*
* #param string $controller A string in the class::method notation
*
* #return string A short notation controller (a:b:c)
*
* #throws \InvalidArgumentException when the controller is not valid or cannot be found in any bundle
*/
My Implementation
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
class ActivityRoleControllerType extends AbstractType
{
...
/**
* Controller choices
*
* #var array
*/
private static $controllers = array();
/**
* Controller Name Parser
*
* #var ControllerNameParser
*/
private $parser;
/**
* expects the service_container service
*/
public function __construct(ContainerInterface $container)
{
$this->parser = new ControllerNameParser($container->get('kernel'));
self::$controllers = $this->getControllerLogicalNames(
$container->get('router')->getRouteCollection()->all(), $this->parser
);
}
/**
* Creates Logical Controller Names for all controllers under \Application\*
* namespace.
*
* #param Route[] $routes The routes to iterate through.
* #param ControllerNameParser $parser The Controller Name parser.
*
* #return array the ChoiceType choices compatible array of Logical Controller Names.
*/
public function getControllerLogicalNames(array $routes, ControllerNameParser $parser)
{
if (! empty(self::$controllers)) {
return self::$controllers;
}
$controllers = array();
/* #var $route \Symfony\Component\Routing\Route */
foreach ($routes as $route) {
$controller = $route->getDefault('_controller')
if (0 === strpos($controller, 'Application\\')) {
try {
$logicalName = $parser->build($controller);
$controllers[$logicalName] = $logicalName;
} catch (\InvalidArgumentException $exc) {
// Do nothing, invalid names skiped
continue;
}
}
}
asort($controllers);
return $controllers;
}
}

Related

Laravel - Add additional information to route

Currently I am working on a project where we are trying to create a RESTful API. This API uses some default classes, for example the ResourceController, for basic behaviour that can be overwritten when needed.
Lets say we have an API resource route:
Route::apiResource('posts', 'ResourceController');
This route will make use of the ResourceController:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Repositories\ResourceRepository;
class ResourceController extends Controller
{
/**
* The resource class.
*
* #var string
*/
private $resourceClass = '\\App\\Http\\Resources\\ResourceResource';
/**
* The resource model class.
*
* #var string
*/
private $resourceModelClass;
/**
* The repository.
*
* #var \App\Repositories\ResourceRepository
*/
private $repository;
/**
* ResourceController constructor.
*
* #param \Illuminate\Http\Request $request
* #return void
*/
public function __construct(Request $request)
{
$this->resourceModelClass = $this->getResourceModelClass($request);
$this->repository = new ResourceRepository($this->resourceModelClass);
$exploded = explode('\\', $this->resourceModelClass);
$resourceModelClassName = array_last($exploded);
if (!empty($resourceModelClassName)) {
$resourceClass = '\\App\\Http\\Resources\\' . $resourceModelClassName . 'Resource';
if (class_exists($resourceClass)) {
$this->resourceClass = $resourceClass;
}
}
}
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, $this->getResourceModelRules());
$resource = $this->repository->create($request->all());
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$resource = $this->repository->show($id);
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
...
/**
* Get the model class of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelClass(Request $request)
{
if (is_null($request->route())) return '';
$uri = $request->route()->uri;
$exploded = explode('/', $uri);
$class = str_singular($exploded[1]);
return '\\App\\Models\\' . ucfirst($class);
}
/**
* Get the model rules of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelRules()
{
$rules = [];
if (method_exists($this->resourceModelClass, 'rules')) {
$rules = $this->resourceModelClass::rules();
}
return $rules;
}
}
As you can maybe tell we are not making use of model route binding and we make use of a repository to do our logic.
As you can also see we make use of some dirty logic, getResourceModelClass(), to determine the model class needed to perform logic on/with. This method is not really flexible and puts limits on the directory structure of the application (very nasty).
A solution could be adding some information about the model class when registrating the route. This could look like:
Route::apiResource('posts', 'ResourceController', [
'modelClass' => Post::class
]);
However it looks like this is not possible.
Does anybody have any suggestions on how to make this work or how to make our logic more clean and flexible. Flexibility and easy of use are important factors.
The nicest way would be to refactor the ResourceController into an abstract class and have a separate controller that extends it - for each resource.
I'm pretty sure that there is no way of passing some context information in routes file.
But you could bind different instances of repositories to your controller. This is generally a good practice, but relying on URL to resolve it is very hacky.
You'd have to put all the dependencies in the constructor:
public function __construct(string $modelPath, ResourceRepository $repo // ...)
{
$this->resourceModelClass = $this->modelPath;
$this->repository = $repo;
// ...
}
And do this in a service provider:
use App\Repositories\ResourceRepository;
use App\Http\Controllers\ResourceController;
// ... model imports
// ...
public function boot()
{
if (request()->path() === 'posts') {
$this->app->bind(ResourceRepository::class, function ($app) {
return new ResourceRepository(new Post);
});
$this->app->when(ResourceController::class)
->needs('$modelPath')
->give(Post::class);
} else if (request()->path() === 'somethingelse') {
// ...
}
}
This will give you more flexibility, but again, relying on pure URL paths is hacky.
I just showed an example for binding the model path and binding a Repo instance, but if you go down this road, you'll want to move all the instantiating out of the Controller constructor.
After a lot of searching and diving in the source code of Laravel I found out the getResourceAction method in the ResourceRegistrar handles the option passed to the route.
Further searching led me to this post where someone else already managed to extend this registrar en add some custom functionality.
My custom registrar looks like:
<?php
namespace App\Http\Routing;
use Illuminate\Routing\ResourceRegistrar as IlluResourceRegistrar;
class ResourceRegistrar extends IlluResourceRegistrar
{
/**
* Get the action array for a resource route.
*
* #param string $resource
* #param string $controller
* #param string $method
* #param array $options
* #return array
*/
protected function getResourceAction($resource, $controller, $method, $options)
{
$action = parent::getResourceAction($resource, $controller, $method, $options);
if (isset($options['model'])) {
$action['model'] = $options['model'];
}
return $action;
}
}
Do not forget to bind in the AppServiceProvider:
$registrar = new ResourceRegistrar($this->app['router']);
$this->app->bind('Illuminate\Routing\ResourceRegistrar', function () use ($registrar) {
return $registrar;
});
This custom registrar allows the following:
Route::apiResource('posts', 'ResourceController', [
'model' => Post::class
]);
And finally we are able to get our model class:
$resourceModelClass = $request->route()->getAction('model');
No hacky url parse logic anymore!

Doctrine Data Type (use Factory)

i want to implement a new doctrine data type (http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html).
I have implemented an Country Service which loads country data via adapter from any library. Know i have the following implementation:
<?php
interface CountryInterface;
interface Address
{
public function setCountry(CountryInterface $country);
public function getCountry() : CountryInterface;
}
?>
So, what I want to do is - make a CountryType which converts the Country Object to an specific string value (used field will be set via OptionClass, ex.: Alpha2, Alpha3, IsoNumber).
My problem is, doctrine only allows data types mapping via classname, so I can't implement an factory to load all needed dependencies.
I hope this is understandable.
regards
First you will need to register your custom DBAL type for country extending the Doctrine\DBAL\Types\Type class:
<?php
namespace Application\DBAL\Types;
use Application\Resource\Country;
use Application\Service\CountryService;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\ConversionException;
use Doctrine\DBAL\Types\Type;
use InvalidArgumentException;
class CountryType extends Type
{
const NAME = 'country';
/**
* Country service
*/
protected $countryService;
/**
* #return string
*/
public function getName()
{
return self::NAME;
}
/**
* {#inheritdoc}
*/
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return $platform->getDoctrineTypeMapping('text');
}
/**
* {#inheritdoc}
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
if ($value instanceof Country) {
return (string) $value;
}
throw ConversionException::conversionFailed($value, self::NAME);
}
/**
* {#inheritdoc}
*/
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if($value === null){
return null;
}
$country = $this->countryService->getCountry($value);
if( ! $country instanceof Country ){
throw ConversionException::conversionFailed($value, self::NAME);
}
return $country;
}
/**
* Set country service
*
* #var CountryService $service
*/
public function setCountryService ($service){
$this->countryService = $service;
}
}
This type needs to implement four methods getName, getSQLDeclaration, convertToDatabaseValue and convertToPHPValue.
Thre first one returns the name for the type
The second one is for the type declaration in the (SQL) database (I used text in the example, but you can also use integer or any other valid doctrine database type).
The third method converts your country object to a database value (so in this case a text value).
The last method does the opposite; it converts the text value from the database. In this case I just instantiate the Country class and pass the database value to the constructor. You need to add your custom logic inside your class constructor.
In my example I assume that null values are also allowed.
A simple version of your Country class could look like this:
<?php
namespace Application\Resource;
class Country{
protected $value;
/**
* Magic stringify to cast country object to a string
*/
public function __toString(){
return $value;
}
/**
* Constructor method
*/
public function construct($value){
$this->value = $value
// set other properties...
}
// setters and getters...
}
It is up to you whether value should be alpha2/alpha3/country_name or whatever you want visible in the database. You should somehow also populate the other country with the other properties in the constructor method. I leave this part up to you.
Now you need to register your custom country type so doctrine will use it:
'doctrine' => array(
//...
'configuration' => array(
'orm_default' => array(
Application\DBAL\Types\CountryType::NAME => Application\DBAL\Types\CountryType::class,
)
)
)
And you can set your service on bootstrap in your application Module.php file:
/**
* #param EventInterface|MvcEvent $event
* #return void
*/
public function onBootstrap(EventInterface $event)
{
$application = $event->getApplication();
$eventManager = $application->getEventManager();
$eventManager->attach(MvcEvent::EVENT_BOOTSTRAP, array($this, 'initializeCountryType');
}
/**
* #param MvcEvent $event
*/
public function initializeCountryType(MvcEvent $event)
{
$application = $event->getApplication();
$serviceManager = $application->getServiceManager();
//get your country service from service manager
$countryService = $serviceManager->getCountryService();
$countryType = \Doctrine\DBAL\Types\Type::getType('country');
$countryType->setCountryService($countryService);
}
Now you can use your country type in any entity definition as follows:
/**
* #var string
* #ORM\Column(type="country", nullable=true)
*/
protected $country;
Read more on how to map custom DBAL types in the Doctrine2 documentation chapter Custom Mapping Types

Where do I place my lengthy code that prepares domain objects to be persisted in a Repository?

I have some code like so:
/* Part of Controller::saveAction() */
//create new object instance
$item = new Item();
//populate the Item
$item->setDescription($description);
$item->setQuantity($quantity);
$item->setPrice($price);
//once we have a fully populated object,
//send it to Repository pattern,
//which saves it to persistent storage
$this->repository->saveItem($item);
Imagine several blocks like the above in a single funciton/method, and you will see my issue ... I am pretty happy with my repository line of code, but I do not know where to place all the "prep work" that is done before the call to Repository.
Question:
Where do I place the bulk of code that creates &populates the Item object instance? It is cluttering up my Controller method and I can't seem to imagine another place to put it.
Goal
My goal is to find good structure/design rather than to purely reduce or minimize the number of "item preparation" lines.
Depending on Controller, I have roughly 5-7 Item instances that each have 10-16 lines of code creating and populating the instances.
Observation
Individual values like $description, $quantity, $price must come from somewhere. It could be GET, POST, SESSION, COOKIES, or database or external methods. Let's call it $_SOMEWHERE for clarity. We then get:
$description = $_SOMEWHERE['description'];
$quantity = $_SOMEWHERE['quantity'];
$price = $_SOMEWHERE['price'];
Define an Input&Preparation class that does the work for you and returns the prepared Item.
class AcquireItem
{
function getItem()
{
$item = new Item();
$item->setDescription($_SOMEWHERE['description']);
$item->setQuantity($_SOMEWHERE['quantity']);
$item->setPrice($_SOMEWHERE['price']);
return $item;
}
}
Controller
$item = (new AcquireItem())->getItem();
$this->repository->saveItem($item);
Controller becomes shorter, by effectively "stuffing away" the bulky clutter code into a class that is concerned with reading and preparing input, and decluttering the controller. The code has to exist somewhere, but might as well be out of view, and elsewhere.
For different types of Item, you can vary the method, i.e. getItemA(), getItemB().
As you mentioned about the proper way of achieving
so.. how about using delegators and splitting the controller (blocks) into delegators? It terms of terminology it would be the Facade [Adapter]
add the following to ./module/MyModule/config/module.config.php:
'controllers' => array(
'invokables' => array(
'MyModule\CreateController' => 'MyModule\Controller\MyController',
'MyModule\ReadController' => 'MyModule\Controller\MyController',
),
'delegators' => array(
'MyModule\CreateController' => array(
'MyModule\Controller\Delegator\CreateItemDelegatorFactory'
),
'MyModule\ReadController' => array(
'MyModule\Controller\Delegator\ReadItemDelegatorFactory'
),
),
),
'form_elements' => array(
'invokables' => array(
'item_create' => 'MyModule\Form\CreateForm',
),
),
Create Delegator loads form, populates it, validates and tries to save teh data
./module/MyModule/src/MyModule/Controller/Delegator/CreateItemDelegatorFactory.php:
namespace MyModule\Controller\Delegator;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use MyModule\Entity\Item as Entity;
/**
* Class loads the form, checks if form been posted and if is valid.
* If form is valid it tries to save the item with repository service
* Class sets the Form per controller, such solution keeps form
* validation messages
*/
class CreateItemDelegatorFactory implements DelegatorFactoryInterface
{
/**
* Determines name of the form to be loaded with formElementManager
*
* #var string
*/
private $form_name = "item_create";
/**
* Name of repository service. It may be database, api or other
*
* #var string
*/
private $service_repository_name = "service.repository";
public function createDelegatorWithName(
ServiceLocatorInterface $serviceLocator,
$name,
$requestedName,
$callback
) {
// assign serviceManager locally
$parentLocator = $serviceLocator->getServiceLocator();
// assign services locally
$routerService = $parentLocator->get('router');
$requestService = $parentLocator->get('request');
// get repository service
$repositoryService = $parentLocator->get($this->service_repository_name);
// read the CreateForm with formElementManager and bind the Entity
$callback->setForm(
$parentLocator->get('FormElementManager')->get($this->form_name)
);
$entity = new Entity;
$callback->getForm($this->form_name)->bind($entity);
// check if data been posted
if($requestService->isPost()) {
$postData = $requestService->getPost($this->form_name);
$callback->getForm($this->form_name)->setData($postData);
// validate form
if($callback->getForm($this->form_name)->isValid()) {
// form is valid
$repositoryService->saveItem($entity);
}
}
}
}
With above Delegator your controller (MyModule\Controller\MyController) would need the additional property and two methods:
/**
* Holds the form object
* #var object
*/
private $form
public function setForm($form=null)
{
$this->form = $form;
return $this;
}
public function getForm()
{
return $this->form;
}
./module/MyModule/src/MyModule/Controller/Delegator/ReadItemDelegatorFactory.php:
namespace MyModule\Controller\Delegator;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use MyModule\Entity\Item as Entity;
/**
* Creates Delegator which tries read item's id from the (segment type) route
* and read the Item from the repository service
*
*/
class ReadItemDelegatorFactory implements DelegatorFactoryInterface
{
/**
* Item's ID from route
*
* #var string
*/
private $route_identifier = "item_id";
/**
* Name of repository service. It may be database, api or other
*
* #var string
*/
private $service_repository_name = "service.repository";
public function createDelegatorWithName(
ServiceLocatorInterface $serviceLocator,
$name,
$requestedName,
$callback
) {
// assign serviceManager locally
$parentLocator = $serviceLocator->getServiceLocator();
// assign services locally
$routerService = $parentLocator->get('router');
$requestService = $parentLocator->get('request');
// get repository service
$repositoryService = $parentLocator->get($this->service_repository_name);
// get the router match and the item_id
$routerMatch = $routerService->match($requestService);
$itemId = $routerMatch->getParam($this->route_identifier);
// set the data for the target controller
$callback->setItem($repositoryService->readItem($itemId));
return $callback;
}
With above Delegator your controller (MyModule\Controller\MyController) would need the additional property and method:
/**
* Holds the Item object
* #var object \MyModule\Entity\Item
*/
private $item
public function setItem($item=null)
{
$this->item = $item;
return $this;
}
Such a way of controller's use helps for the code to stay DRY and seems to be possible to control the flow. Delegators are being loaded as LIFO, so it is possiblt to preconfigure the controller ($callback) before passing it to another Delegator.
If the ReadController reads the item and the CreateController loads the form it is a short way for UpdateItemDelegator to handle the Item Update task.
'controllers' => array(
'invokables' => array(
'MyModule\UpdateController' => 'MyModule\Controller\MyController',
'delegators' => array(
'MyModule\ReadController' => array(
'MyModule\Controller\Delegator\UpdateItemDelegatorFactory',
'MyModule\Controller\Delegator\CreateItemDelegatorFactory',
'MyModule\Controller\Delegator\ReadItemDelegatorFactory'
),
),
),
Delegators explained:
http://ocramius.github.io/blog/zend-framework-2-delegator-factories-explained/
Edit:
Controller prepared for both Delegators (Create and Read) would look like:
namespace MyModule\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class CreateController extends AbstractActionController
{
/**
* Holds the Item object
* #var object \MyModule\Entity\Item
*/
private $item
/**
* Holds the form object
* #var mixed null|object
*/
private $form;
/**
* Item's details. It reads item by the `item_id` which is param set in route with same name
* #return \Zend\View\Model\ViewModel
*/
public function readAction()
{
$v = new ViewModel();
// set item, access it in template as `$this->item`
$v->setVariable('item',$this->getItem());
return $v;
}
/**
* The Form preconfigured with CreateItemDelegatorFactory should be av. in template as `$this->form`
* #return \Zend\View\Model\ViewModel
*/
public function createAction()
{
$v = new ViewModel();
// set form, access the in template as `$this->form`
$v->setVariable('form',$this->getForm());
return $v;
}
/**
* Sets the Item object
* #var $item \MyModule\Entity\Item
* #return $this
*/
public function setItem($item=null)
{
$this->item = $item;
return $this;
}
/**
* Gets the Item object
* #return object \MyModule\Entity\Item
*/
public function getItem()
{
return $this->item;
}
public function setForm($form=null)
{
$this->form = $form;
return $this;
}
/**
* Returns form defined in config and CreateItemDelegatorFactory::form_name
* #return \Zend\Form\Form
*/
public function getForm()
{
return $this->form;
}
}
I personally believe that create-methods should go into the repository.
That is because I would expect the repositories to contain all CRUD (create, read, update, delete) methods.
It's just my personal thought on this topic...

Designing a service container: SRP, choosing the right creational pattern and avoiding tight coupling

I'm writing my own implementation of the Laravel Service Container to practice some design patterns and later make a private microframework.
The class looks like this right now:
class Container implements ContainerInterface
{
/**
* Concrete bindings of contracts.
*
* #var array
*/
protected $bindings = [];
/**
* Lists of arguments used for a class instantiation.
*
* #var array
*/
protected $arguments = [];
/**
* Container's storage used to store already built or customly setted objects.
*
* #var array
*/
protected $storage = [];
/**
* Returns an instance of a service
*
* #param $name
* #return object
* #throws \ReflectionException
*/
public function get($name) {
$className = (isset($this->bindings[$name])) ? $this->bindings[$name] : $name;
if (isset($this->storage[$className])) {
return $this->storage[$className];
}
return $this->make($className);
}
/**
* Creates an instance of a class
*
* #param $className
* #return object
* #throws \ReflectionException
*/
public function make($className) {
$refObject = new \ReflectionClass($className);
if (!$refObject->isInstantiable()) {
throw new \ReflectionException("$className is not instantiable");
}
$refConstructor = $refObject->getConstructor();
$refParameters = ($refConstructor) ? $refConstructor->getParameters() : [];
$args = [];
// Iterates over constructor arguments, checks for custom defined parameters
// and builds $args array
foreach ($refParameters as $refParameter) {
$refClass = $refParameter->getClass();
$parameterName = $refParameter->name;
$parameterValue =
isset($this->arguments[$className][$parameterName]) ? $this->arguments[$className][$parameterName]
: (null !== $refClass ? $refClass->name
: ($refParameter->isOptional() ? $refParameter->getDefaultValue()
: null));
// Recursively gets needed objects for a class instantiation
$args[] = ($refClass) ? $this->get($parameterValue)
: $parameterValue;
}
$instance = $refObject->newInstanceArgs($args);
$this->storage[$className] = $instance;
return $instance;
}
/**
* Sets a concrete implementation of a contract
*
* #param $abstract
* #param $concrete
*/
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
/**
* Sets arguments used for a class instantiation
*
* #param $className
* #param array $arguments
*/
public function setArguments($className, array $arguments) {
$this->arguments[$className] = $arguments;
}
}
It works fine but I clearly see a violation of SRP in the make() method. So I decided to delegate an object creational logic to a separate class.
A problem that I encountered is that this class will be tightly coupled with a Container class. Because it needs an access to $bindings and $arguments arrays, and the get() method. And even if we pass these parameters to the class, the storage still stays in a container. So basically all architecture is wrong and we need, like, 2 more classes: StorageManager and ClassFactory. Or maybe ClassBuilder? And should ClassFactory be able to build constructor arguments or it needs another class — ArgumentFactory?
What do you think guys?

How to unit test a php class constructor that returns no value

I'm a little bit confused about how to unit test a constructor, particularly since it returns no value.
Let's assume I have this class:
class MyClass {
/** #var array */
public $registered_items;
/**
* Register all of the items upon instantiation
*
* #param array $myArrayOfItems an array of objects
*/
public function __construct($myArrayOfItems) {
foreach($myArrayOfItems as $myItem) {
$this->registerItem($myItem);
}
}
/**
* Register a single item
*
* #param object $item a single item with properties 'slug' and 'data'
*/
private function registerItem($item) {
$this->registered_items[$item->slug] = $item->data;
}
}
Obviously this is a bit contrived and incredibly simple, but it's for the sake of the question. =)
So yeah, how would I go about writing a unit test for the constructor here?
Bonus question: am I right in thinking that no unit test for registerItem() would be needed in a case such as this?
EDIT
How about if I re-factored to remove the logic from the constructor. How would I test registerItem() in this case?
class MyClass {
/** #var array */
public $registered_items;
public function __construct() {
// Nothing at the moment
}
/**
* Register all of the items
*
* #param array $myArrayOfItems an array of objects
*/
public function registerItem($myArrayOfItems) {
foreach($myArrayOfItems as $item) {
$this->registered_items[$item->slug] = $item->data;
}
}
}
Add a method to look up a registered item.
class MyClass {
...
/**
* Returns a registered item
*
* #param string $slug unique slug of the item to retrieve
* #return object the matching registered item or null
*/
public function getRegisteredItem($slug) {
return isset($this->registered_items[$slug]) ? $this->registered_items[$slug] : null;
}
}
Then check that each item passed to the constructor in the test has been registered.
class MyClassTest {
public function testConstructorRegistersItems() {
$item = new Item('slug');
$fixture = new MyClass(array($item));
assertThat($fixture->getRegisteredItem('slug'), identicalTo($item));
}
}
Note: I'm using the Hamcrest assertions, but PHPUnit should have an equivalent.
For First Code
public function testConstruct{
$arrayOfItems = your array;
$myClass = new MyClass($arrayOfItems);
foreach($arrayOfItems as $myItem) {
$expected_registered_items[$item->slug] = $item->data;
}
$this->assertEquals($expected_registered_items, $myClass->registered_items);
}

Categories