Slim Framework: routes and controllers - php

Originally, my Slim Framework app had the classic structure
(index.php)
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', function ($name) {
echo "Hello, $name";
});
$app->run();
But as I added more routes and groups of routes, I moved to a controller based approach:
index.php
<?php
$app = new \Slim\Slim();
$app->get('/hello/:name', 'HelloController::hello');
$app->run();
HelloController.php
<?php
class HelloController {
public static function hello($name) {
echo "Hello, $name";
}
}
This works, and it had been helpful to organize my app structure, while at the same time lets me build unit tests for each controler method.
However, I'm not sure this is the right way. I feel like I'm mocking Silex's mount method on a sui generis basis, and that can't be good. Using the $app context inside each Controller method requires me to use \Slim\Slim::getInstance(), which seems less efficient than just using $app like a closure can.
So... is there a solution allowing for both efficiency and order, or does efficiency come at the cost of route/closure nightmare?

I guess I can share what I did with you guys. I noticed that every route method in Slim\Slim at some point called the method mapRoute
(I changed the indentation of the official source code for clarity)
Slim.php
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Slim\Route(
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
In turn, the Slim\Route constructor called setCallable
Route.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class;
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
Which is basically
If $callable is a string and (mind the single colon) has the format ClassName:method then it's non static, so Slim will instantiate the class and then call the method on it.
If it's not callable, then throw an exception (reasonable enough)
Otherwise, whatever it is (ClassName::staticMethod, closure, function name) it will be used as-is.
ClassName should be the FQCN, so it's more like \MyProject\Controllers\ClassName.
The point where the controller (or whatever) is instantiated was a good opportunity to inject the App instance. So, for starters, I overrode mapRoute to inject the app instance to it:
\Util\MySlim
protected function mapRoute($args)
{
$pattern = array_shift($args);
$callable = array_pop($args);
$route = new \Util\MyRoute(
$this, // <-- now my routes have a reference to the App
$pattern,
$callable,
$this->settings['routes.case_sensitive']
);
$this->router->map($route);
if (count($args) > 0) {
$route->setMiddleware($args);
}
return $route;
}
So basically \Util\MyRoute is \Slim\Route with an extra parameter in its constructor that I store as $this->app
At this point, getCallable can inject the app into every controller that needs to be instantiated
\Util\MyRoute.php
public function setCallable($callable)
{
$matches = [];
$app = $this->app;
if (
is_string($callable) &&
preg_match(
'!^([^\:]+)\:([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$!',
$callable,
$matches
)
) {
$class = $matches[1];
$method = $matches[2];
$callable = function () use ($app, $class, $method) {
static $obj = null;
if ($obj === null) {
$obj = new $class($app); // <--- now they have the App too!!
}
return call_user_func_array([$obj, $method], func_get_args());
};
}
if (!is_callable($callable)) {
throw new \InvalidArgumentException('Route callable must be callable');
}
$this->callable = $callable;
}
So there it is. Using this two classes I can have $app injected into whatever Controller I declare on the route, as long as I use a single colon to separate controller from method. Using paamayim nekudotayim will call the method as static and therefore will throw an error if I try to access $this->app inside it.
I ran tests using blackfire.io and... the performance gain is negligible.
Pros:
this saves me the pain of calling $app = \Slim\Slim::getInstance() on every static method call accounting for about 100 lines of text overall.
it opens the way for further optimization by making every controller inherit from an abstract controller class, which in turn wraps the app methods into convenience methods.
it made me understand Slim's request and response lifecycle a little better.
Cons:
performance gains are negligible
you have to convert all your routes to use a single colon instead of paamayin, and all your controller methods from static to dynamic.
inheritance from Slim base classes might break when they roll out v 3.0.0
Epilogue: (4 years later)
In Slim v3 they removed the static accessor. In turn, the controllers are instantiated with the app's container, if you use the same convention FQCN\ClassName:method. Also, the method receives the request, response and $args from the route. Such DI, much IoC. I like it a lot.
Looking back on my approach for Slim 2, it broke the most basic principle of drop in replacement (Liskov Substitution).
class Route extends \Slim\Route
{
protected $app;
public function __construct($app, $pattern, $callable, $caseSensitive = true) {
...
}
}
It should have been
class Route extends \Slim\Route
{
protected $app;
public function __construct($pattern, $callable, $caseSensitive = true, $app = null) {
...
}
}
So it wouldn't break the contract and could be used transparently.

Related

Test mockery with a pipeline

How can I assert this:
$this->assertEquals('incoming', $payload['routerAction']);
Skipping the pipelines. I'm using phpleague/pipeline
Code:
class IncomingPipeline
{
public function __invoke(array $payload)
{
$router = $payload['router'];
$payload['routerAction'] = 'incoming';
return (new Pipeline())
->pipe(new DispatchIncomingEventStage())
->pipe(BaseRouter::route($router))
->process($payload);
}
}
I basically want to skip the Pipeline OR set andReturn($payload) on them.
You could make the pipeline a dependency to IncomingPipeline and pass it into the constructor. That way, you could inject a pipeline without any stages in your tests.
If you want to keep it the way it is, you can use the overload prefix (docs):
final class IncomingPipelineTest extends TestCase
{
public function test()
{
$pipeline = Mockery::mock('overload:' . Pipeline::class);
$pipeline->allows('pipe')->andReturnSelf();
$pipeline->allows('process')->andReturnArg(0);
$payload = (new IncomingPipeline())(...);
self::assertEquals('incoming', $payload['routerAction']);
}
}

Should a custom find method on a Laravel Model be static?

In the following Laravel 5 Model should the findByIdAndCourseOrFail method be static?
class Section extends Model {
//should this method be static?
public function findByIdAndCourseOrFail($id, $courseId)
{
$result = $this->where('id', $id)->where('course_id', $courseId)->first();
if (!is_null($result))
{
return $result;
}
throw (new ModelNotFoundException())->setModel(Section::class);
}
}
With the controller:
class SectionsController extends Controller {
protected $sections;
public function __construct(Section $section)
{
$this->sections = $section;
}
public function foo($id, $courseId) //illustration only
{
$section = $this->sections->findOrFail($id);
$section = $this->sections->findByIdAndCourseOrFail($id, $courseId);
//would need to be non-static
$section = Section::findByIdAndCourseOrFail($id, $courseId);
//weird when compared with find above
}
On the one hand, we're not acting on a Section instance [See Note]. On the other hand, in a controller with auto-dependency injection through Laravel's service container we'd act on an instance: $sections = $this->sections-> findByIdAndCourseOrFail(7,3); and my IDE (PhpStorm) squawks if Static.
[Note]: This comment may be a misunderstanding of how Laravel Models work. For me, I would expect that find(), findOrFail() to be Class methods and thus Static as opposed to the instance that a find method would return.
I'm not sure if local scopes are meant to be used like that. But it works for me on laravel 5.2:
public function scopeFindByIdAndCourseOrFail($query, $id, $courseId)
{
$result = $query->where('id', $id)->where('course_id', $courseId)->first();
if (!is_null($result))
{
return $result;
}
throw (new ModelNotFoundException())->setModel(Section::class);
}
In the controller you can use it both ways:
$section = Section::findByIdAndCourseOrFail($id, $courseId);
Or
$model = new Section();
$section = $model->findByIdAndCourseOrFail($id, $courseId);
class Section extends Model {
public static function findByIdAndCourseOrFail($id, $courseId)
{
$result = self::where('id', $id)->where('course_id', $courseId)->first();
if (!is_null($result))
{
return $result;
}
throw (new ModelNotFoundException())->setModel(Section::class);
}
}
Personally I would make this a static method, I'm not sure if there is a "correct" answer though as either can be done. The way I kind of separate them in my mind is if I'm doing something to an instance of a model then I make it a normal public function. If I am doing something to the Collection I use a static. For example:
$person = new Person();
$person->setAdmin(true);
$person->save();
// OR
$admins = Person::getAdmins();
In the first example we have a specific instance of a Person and we are manipulating it, all code would be simply manipulating that specific instance. In the second example we are acting on the entire collection of Person and we want a collection of objects to be returned.
In your case you would have to initiate an instance of Section just to be able to use your non-static public method, like this:
$section = new Section();
$foundSection = $section->findByIdAndCourseOrFail(7,3);
So $section becomes a temporary variable that is never really used. On the other hand if you made it a static you could call it without having to do this.
$section = Section::findByIdAndCourseOrFail(7,3);
Hopefully that makes sense.

Slim PHP Framework: can middleware be lazy-loaded?

While building a small Slim app, I noticed that all my middleware instances are getting created for every request. I have four routes in my app, only one of which requires a PDO instance, but all of which create an instance. Another route requires a SoapClient instance, but again, requests to any route result in instances being created. Can I do something differently to avoid that?
Here is a stripped down example showing what I mean:
<?php
require __DIR__ . '/../vendor/autoload.php';
class Middleware {
private $dep;
public function __construct($dep = null) {
$this->dep = $dep;
}
public function __invoke($req, $res) {
$res->getBody()->write($this->dep ? 'Got dependency' : 'No dependency');
return $res;
}
}
$app = new \Slim\App();
$c = $app->getContainer();
$c['pdo'] = function ($c) {
echo "PDO connection here<br/>\n";
return (object) ['pdo' => true];
};
$c['middleware.nodep'] = function ($c) {
return new Middleware();
};
$c['middleware.withdep'] = function ($c) {
return new Middleware($c['pdo']);
};
$app->get('/nopdo', function () {})->add($c['middleware.nodep']);
$app->get('/withpdo', function () {})->add($c['middleware.withdep']);
$app->run();
Requests to /withpdo get the following response, as you would expect:
PDO connection hereGot dependency
Requests to /nopdo get this, which I don't want:
PDO connection here No dependency
So, is there a way to only instantiate the "PDO" instance for requests to /withpdo?
You are calling the factory in add(), rather than just telling Slim about the DIC key.
i.e. change:
->add($c['middleware.withdep']);
to
->add('middleware.withdep');
This way, Slim will ask the DIC for 'middleware.withdep' when it needs it.

PHP DRY Throwing InvalidArgumentException

In a framework I am building, I am moving towards making my code more testable as I was previously addicted to the MVC+Singleton pattern and had static classes galore. Since then, I have started to understand more about Unit Testing and TDD so it prompted me to re-factor a lot of code. Part of this re-factoring has driven me to try and use the Extension classes within PHP properly, i.e. not just throw the Exception class but more relevant exceptions.
I have the following class:
<?php
namespace Framework;
class Uri {
public static function new_from_http() {
$uri = '';
if (isset($_SERVER['REQUEST_URI'])) {
$uri = $_SERVER['REQUEST_URI'];
} elseif (isset($_SERVER['PATH_INFO'])) {
$uri = $_SERVER['PATH_INFO'];
}
return static::new_from_string($uri);
}
public static function new_from_string($string) {
return new static(explode('/', $string));
}
protected $uri = [];
public function __construct(array $uri) {
$this->uri = array_values(array_filter($uri));
}
public function get_segment($offset, $default = null) {
if (!is_int($offset)) {
throw new \InvalidArgumentException(
sprintf('%s requires argument 1 to be an integer, %s given.',
__METHOD__,
gettype()
)
);
}
return isset($this->uri[$offset - 1])
? $this->uri[$offset - 1]
: $default;
}
}
Thats all well and good and as you can see, the get_segment method requires an integer otherwise it throws an InvalidArgumentException. The trouble is, I want to create a few more methods which also require integers as an argument and I do not want to cut and paste that code everywhere. What are the best options of consolidating all of these types of argument check so that I can use them within different classes and methods while keeping the messages consistent with one another.
One of my ideas was to extend the exception classes under the framework namespace and have the constructor take different parameters such as:
namespace Framework;
class InvalidArgumentException extends \InvalidArgumentException {
public function __construct($method, $argument, $value) {
parent::__construct(
sprintf('%s requires argument 1 to be an integer, %s given.',
$method,
gettype($value)
)
);
}
}
Which would be called like:
if (!is_int($arg)) {
throw new \Framework\InvalidArgumentException(__METHOD__, 1, $arg);
}
It could also be improved that the \Framework\InvalidArgumentException can get the __METHOD__ value via a back trace.
What other options do I have and what is the best one?
I would extend the /InvalidArgumentException into a NonIntegerException doing basically the same thing otherwise. This way if you wanted to require strings, arrays or any other type, you are able to create new exceptions and you don't have to have crazy logic for determining which message to use.

How to inject mock classes into controllers (without having the controller aware of the tests)

This is a follow-on from a previous question I had: How to decouple my data layer better and restrict the scope of my unit tests?
I've read around on Zend and DI/IoC and came up with the following changes to my code:
Module Bootstrap
class Api_Bootstrap extends Zend_Application_Module_Bootstrap
{
protected function _initAllowedMethods()
{
$front = Zend_Controller_Front::getInstance();
$front->setParam('api_allowedMethods', array('POST'));
}
protected function _initResourceLoader()
{
$resourceLoader = $this->getResourceLoader();
$resourceLoader->addResourceType('actionhelper', 'controllers/helpers', 'Controller_Action_Helper');
}
protected function _initActionHelpers()
{
Zend_Controller_Action_HelperBroker::addHelper(new Api_Controller_Action_Helper_Model());
}
}
Action Helper
class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
{
public function preDispatch()
{
if ($this->_actionController->getRequest()->getModuleName() != 'api') {
return;
}
$this->_actionController->addMapper('account', new Application_Model_Mapper_Account());
$this->_actionController->addMapper('product', new Application_Model_Mapper_Product());
$this->_actionController->addMapper('subscription', new Application_Model_Mapper_Subscription());
}
}
Controller
class Api_AuthController extends AMH_Controller
{
protected $_mappers = array();
public function addMapper($name, $mapper)
{
$this->_mappers[$name] = $mapper;
}
public function validateUserAction()
{
// stuff
$accounts = $this->_mappers['account']->find(array('username' => $username, 'password' => $password));
// stuff
}
}
So, now, the controller doesn't care what specific classes the mappers are - so long as there is a mapper...
But how do I now replace those classes with mocks for unit-testing without making the application/controller aware that it is being tested? All I can think of is putting something in the action helper to detect the current application enviroment and load the mocks directly:
class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
{
public function preDispatch()
{
if ($this->_actionController->getRequest()->getModuleName() != 'api') {
return;
}
if (APPLICATION_ENV != 'testing') {
$this->_actionController->addMapper('account', new Application_Model_Mapper_Account());
$this->_actionController->addMapper('product', new Application_Model_Mapper_Product());
$this->_actionController->addMapper('subscription', new Application_Model_Mapper_Subscription());
} else {
$this->_actionController->addMapper('account', new Application_Model_Mapper_AccountMock());
$this->_actionController->addMapper('product', new Application_Model_Mapper_ProductMock());
$this->_actionController->addMapper('subscription', new Application_Model_Mapper_SubscriptionMock());
}
}
}
This just seems wrong...
It is wrong, your system under test shouldn't have any knowledge of mock objects at all.
Thankfully, because you have DI in place, it doesn't have to. Just instantiate your object in the test, and use addMapper() to replace the default mappers with mocked versions.
Your test case should look something like:
public function testBlah()
{
$helper_model = new Api_Controller_Action_Helper_Model;
$helper_model->_actionController->addMapper('account', new Application_Model_Mapper_AccountMock());
$helper_model->_actionController->addMapper('product', new Application_Model_Mapper_ProductMock());
$helper_model->_actionController->addMapper('subscription', new Application_Model_Mapper_SubscriptionMock());
// test code...
}
You could also put this code in your setUp() method so that you don't have to repeat it for every test.
So, after a few misses, I settled on rewriting the action helper:
class Api_Controller_Action_Helper_Model extends Zend_Controller_Action_Helper_Abstract
{
public function preDispatch()
{
if ($this->_actionController->getRequest()->getModuleName() != 'api') {
return;
}
$registry = Zend_Registry::getInstance();
$mappers = array();
if ($registry->offsetExists('mappers')) {
$mappers = $registry->get('mappers');
}
$this->_actionController->addMapper('account', (isset($mappers['account']) ? $mappers['account'] : new Application_Model_Mapper_Account()));
$this->_actionController->addMapper('product', (isset($mappers['product']) ? $mappers['product'] : new Application_Model_Mapper_Product()));
$this->_actionController->addMapper('subscription', (isset($mappers['subscription']) ? $mappers['subscription'] : new Application_Model_Mapper_Subscription()));
}
}
This means that I can inject any class I like via the registry, but have a default/fallback to the actual mapper.
My test case is:
public function testPostValidateAccount($message)
{
$request = $this->getRequest();
$request->setMethod('POST');
$request->setRawBody(file_get_contents($message));
$account = $this->getMock('Application_Model_Account');
$accountMapper = $this->getMock('Application_Model_Mapper_Account');
$accountMapper->expects($this->any())
->method('find')
->with($this->equalTo(array('username' => 'sjones', 'password' => 'test')))
->will($this->returnValue($accountMapper));
$accountMapper->expects($this->any())
->method('count')
->will($this->returnValue(1));
$accountMapper->expects($this->any())
->method('offsetGet')
->with($this->equalTo(0))
->will($this->returnValue($account));
Zend_Registry::set('mappers', array(
'account' => $accountMapper,
));
$this->dispatch('/api/auth/validate-user');
$this->assertModule('api');
$this->assertController('auth');
$this->assertAction('validate-user');
$this->assertResponseCode(200);
$expectedResponse = file_get_contents(dirname(__FILE__) . '/_testPostValidateAccount/response.xml');
$this->assertEquals($expectedResponse, $this->getResponse()->outputBody());
}
And I make sure that I clear the default Zend_Registry instance in my tearDown()
Below is my solution to inject a mocked timestamp for a ControllerTest unit test, which is similar to the question originally posted above.
In the ControllerTest class, a $mockDateTime is instantiated and added as a parameter to the FrontController before calling dispatch().
public function testControllerAction() {
....
$mockDateTime = new DateTime('2011-01-01T12:34:56+10:30');
$this->getFrontController()->setParam('datetime', $mockDateTime);
$this->dispatch('/module/controller/action');
...
}
In the Controller class, dispatch() will pass any parameters into _setInvokeArgs(), which we extend here:
protected function _setInvokeArgs(array $args = array())
{
$this->_datetime = isset($args['datetime']) ? $args['datetime'] : new DateTime();
return parent::_setInvokeArgs($args);
}
The major advantage of this solution is that it allows dependency injection while it does not require the unit tests to clean up global state.

Categories