I have the following DemoController
class DemoController {
public function test() {
return new Response('This is a test!');
}
}
I want to bind this controller to $app ['demo.controller']
$app ['demo.controller'] = $app->share ( function () use($app) {
return new DemoController ();
} );
Inside the DemoController i want to have the Application $app object to work with registered services. What is the right way? Currently i am using __construct($app) for the DemoController and pass $app. This looks like
$app ['demo.controller'] = $app->share ( function () use($app) {
return new DemoController ($app);
} );
What is the best-practice for that?
That is certainly one way to do it. I want to show two alternatives.
One is to get the application injected into the action method directly using a type hint:
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
class DemoController
{
public function test(Request $request, Application $app)
{
$body = 'This is a test!';
$body .= ':'.$request->getRequestUri();
$body .= ':'.$app['foo']->bar();
return new Response($body);
}
}
The advantage of this option is that you don't actually need to register your controller as a service.
The other possibility is to inject the specific service instead of injecting the whole container:
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
class DemoController
{
private $foo;
public function __construct(Foo $foo)
{
$this->foo = $foo;
}
public function test()
{
return new Response($this->foo->bar());
}
}
Service definition:
$app['demo.controller'] = $app->share(function ($app) {
return new DemoController($app['foo']);
});
The advantage of this option is that your controller no longer depends on silex, the container or any particular service name. This makes it more isolated, re-usable and easier to test in isolation.
Related
I know this looks like a dupllicate of Inject Silex $app in my custom class and others, but I couldn't get it working from their solutions.
I define my service like this:
$app['user.repo'] = function () {
return new MyApp\Repository\User();
};
My class looks like this:
<?php
namespace MyApp\Repository;
use Silex\Application;
class User {
public function findAll(Application $app) {
$users = $app['db']->fetchAll('SELECT * FROM user');
return $users;
}
}
And I use the service like this:
$users = $app['user.repo']->findAll($app);
How can I do this same thing without putting $app in all my methods?
Why don't you inject it?
$app['user.repo'] = function () use ($app) {
return new MyApp\Repository\User($app);
};
And here's your modified class:
<?php
namespace MyApp\Repository;
use Silex\Application;
class User {
/** #var Application */
protected $app;
public function __construct(Application $app) {
$this->app = $app;
}
public function findAll() {
$users = $app['db']->fetchAll('SELECT * FROM user');
return $users;
}
}
Or even better: instead of injecting the whole application (and thus hiding your real dependencies, making unit testing a pain), only inject what you really need:
$app['user.repo'] = function () use ($app) {
return new MyApp\Repository\User($app["db"]);
};
This way your class becomes:
<?php
namespace MyApp\Repository;
use Silex\Application;
class User {
protected $db;
public function __construct($db) {
$this->db = $db;
}
public function findAll() {
$users = $this->db->fetchAll('SELECT * FROM user');
return $users;
}
}
I am working on a Slim Framework based API. Following the mvc pattern, I want my routes to be controller driven with logger and renderer injected to every controller.
As a start point I checked out a number of example mvc slim skeletons, and decided to base my structor on one particular tutorial and sample project ( http://jgrundner.com/slim-oo-004-controller-classes/ )
In this setup, injection is done by adding the router controllers to the app container like this:
$container = $app->getContainer();
$container['\App\Controllers\DefaultController'] = function($c){
return new \App\Controllers\DefaultController(
$c->get('logger'),
$c->get('renderer')
);
};
This then allow for a nice clean route and controller:
route e.g.:
$app->get('/[{name}]', '\App\Controllers\DefaultController:index');
controller e.g.:
namespace App\Controllers;
use Psr\Log\LoggerInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class DefaultController{
private $logger;
private $renderer;
public function __construct(LoggerInterface $logger, $renderer){
$this->logger = $logger;
$this->renderer = $renderer;
}
public function index(RequestInterface $request, ResponseInterface $response, $args){
// Log message
$this->logger->info("Slim-Skeleton '/' route");
// Render index view
return $this->renderer->render($response, 'index.phtml', $args);
}
public function throwException(RequestInterface $request, ResponseInterface $response, array $args){
$this->logger->info("Slim-Skeleton '/throw' route");
throw new \Exception('testing errors 1.2.3..');
}
}
Extending the default controller keeps the controllers neat, but requires adding every new controller object to the app container first, which seems inefficient and messy when you have a lot of classes.
Is there a better way?
You can make basecontroller that contain the acontainer.
<?php
namespace App\Controller;
use Slim\Container;
class Controller
{
var $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function __get($var)
{
return $this->container->{$var};
}
}
and the container:
<?php
$container['App\Controller\Controller'] = function ($c) {
return new App\Controller\Controller($c);
};
And the controller
<?php
namespace App\Controllers;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use App\Controller\Controller;
class DefaultController extends Controller{
}
sorry for my english.
stack: Slim 3 framework + Eloquent ORM.
Eloquent works as expected with Slim.
I want to use sort of a MVC pattern where thin controllers and fat models(all db queries and other heavy logic).
All I found is how to use it from routes like this:
$app->get('/loans', function () use ($app) {
$data = DB::table('loan_instalment')->get(); // works
$data = $this->db->table('loan_instalment')->get(); // works
...
}
What I want is ability to call public methods from choosen model, something like this:
use \src\models\Instalment;
$app->get('/loans', function () use ($app) {
$data = $this->model('Instalment')->getSomething(12);
...
}
and Model class is:
namespace src\models;
use Illuminate\Database\Eloquent\Model as Model;
use Illuminate\Database\Capsule\Manager as DB;
class Instalment extends Model
{
protected $table = 'loan_instalment';
public function getSomething($id)
{
return $this->table->find($id);
}
// bunch of other methods
}
My app looks like basic Slim skeleton, Eloquent settings:
$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection($container['settings']['db']);
$capsule->setAsGlobal();
$capsule->bootEloquent();
$container['db'] = function ($container) use ($capsule){
return $capsule;
};
Is it possible ?
If you want to use MVC pattern, you need to make base controller.
<?php
namespace App\Controller;
use Slim\Container;
class BaseController
{
protected $container;
public function __construct(Container $container)
{
$this->container = $container;
}
public function getContainer()
{
return $this->container;
}
public function __get($name)
{
return $this->container->{$name};
}
public function __set($name, $value)
{
$this->container->{$name} = $value;
}
}
And the container:
// Base Controller
$container[App\Controller\BaseController::class] = function ($c) {
return new App\Controller\BaseController($c);
};
$capsule = new \Illuminate\Database\Capsule\Manager;
$capsule->addConnection($container['settings']['db']);
$capsule->setAsGlobal();
$capsule->bootEloquent();
$container['db'] = function ($container) use ($capsule){
return $capsule;
};
Highly recommended to use static function on models
<?php
namespace App\models;
use Illuminate\Database\Eloquent\Model as Model;
use Illuminate\Database\Capsule\Manager as DB;
class Instalment extends Model
{
protected $table = 'loan_instalment';
public static function getSomething($id)
{
return Instalment::find($id);
}
}
And now you code become:
<?php
use App\models\Instalment;
$app->get('/loans', function ($request, $response, $args) {
$data = Instalment::getSomething(12);
...
}
The controller:
<?php
namespace App\Controller;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\models\Instalment;
class HomeController extends BaseController
{
public function __invoke(Request $request, Response $response, Array $args)
{
$data = Instalment::getSomething(12);
// load the template
return $response;
}
}
And the route for the controller
<?php
$app->get('/', App\Controller\HomeController::class);
It looks cleaner, isn't it?
More tutorial:
My Blog
Rob Allen's Blog
You could use the abbility of Slim to use controllers.
Make a basic controller:
// BasicController.php
<?php
namespace src\Controllers;
class BasicController
{
public function model(string $model)
{
return new $model();
}
}
and then in your controllers extend this class and add it to the slim container
//SomeController.php
<?php
namespace src\Controllers;
use \Psr\Http\Message\ServerRequestInterface as Request;
use \Psr\Http\Message\ResponseInterface as Response;
use \src\models\Instalment as Instalment;
class SomeController extends BasicController
{
public function index(Request $request, Response $response, $args)
{
$this->model(Instalment::class)->getSomethingOutOfDB;
//do something else
}
}
...
//container.php
use \Slim\Container as Container;
$container[\src\Controllers\HomeController::class] = function(Container $container) {
return new \src\Controllers\Homecontroller();
}
...
...
//routes.php
$app->get('/someroute', \src\Controllers\HomeController::class . ':index');
...
Another possibility is to extend your \Slim\App by:
//newApp.php
namespace scr\App
class newApp extends \Slim\App
{
public function model(string $model)
{
return new $model();
}
}
I actually would advice against these both methods, and not load your models in this way, since this is considered bad practice.
It is better just to use:
//routes.php
...
use src\Models\Instalment;
...
...
$app->get('/someroute', function() {
$instalment = new Instalment();
// do something with it...
});
I have a base controller that looks something like this:
<?php
namespace framework;
class BaseController
{
public $model;
public $view;
function __construct()
{
$this->model = new ModelFactory();
$this->view = new View($this->model->data);
}
}
This controller is never called directly, but only through extends:
<?php
namespace framework\controllers;
use framework\BaseController,
framework\Router;
class IndexController extends BaseController
{
}
What I'd like to do, and hopefully this makes sense, is insert data or extra functionality into the base controller between $this->model and $this->view in such a way that it's anonymous or decoupled, rather than hard-coded. As an example, the application may or may not require user data. The following is how I could hard code it although this is exactly what I'm trying to avoid:
<?php
namespace framework;
class BaseController
{
public $model;
public $view;
function __construct()
{
$this->model = new ModelFactory();
// get user data
$this->model->data['user_roles'] = array();
if ($user = $this->isLoggedIn()) {
$this->model->data['user_roles'] = $user->roles;
}
$this->view = new View($this->model->data);
}
// check if a user is logged in and return a user object or false
public function isLoggedIn() {}
}
And the following is pseudo code of what my brain thinks I'd like to accomplish:
// anonymous function in my bootstrap or global configuration file
$user_roles = function () {};
<?php
namespace framework;
class BaseController
{
public $model;
public $view;
function __construct($name, $value) // not sure how these are passed in
{
$this->model = new ModelFactory();
// get extra data
$this->model->data[$name] = $value($this->model);
$this->view = new View($this->model->data);
}
}
Not sure if I need a specific pattern or if I'm even heading in the right direction. How can I accomplish something along these lines? I open to any recommended alternative approaches.
What you're looking for are PHP traits.
Traits are a way to extract/isolate behavior which you can then "inject" in any class.
When you declare a trait:
trait ControllerQueryable
{
public function magicQuery()
{ // $this is the class that's using this trait.
$this->orm->fetch(str_replace(get_class($this), 'Controller', ''), $this->params['id']);
}
}
It's made available for any class to use:
class BlogController extends BaseController
{
use ControllerQueryable;
public function showAction()
{
$model = $this->magicQuery();
// yada yada yada
}
}
Contrarily to extends, you can use as many traits as you want.
How can I access a service inside of a Bundle constructor? I'm trying to create a system where a theme bundle can register itself automatically with the theme service, see small example below (the simpler solution the better):
<?php
namespace Organization\Theme\BasicBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ThemeBasicBundle extends Bundle
{
public function __construct() {
$themes = $this->get('organization.themes');
$themes->register(new Organization\Theme\BasicBundle\Entity\Theme(__DIR__));
}
}
However, $this->get does not work, this might be because there is no guarantee that all bundles has been registered yet, are there any post bundle registration "hooks" that I can use instead? Are there any special method names that I can add to the bundle class that gets executed after all bundles has been instantiated?
The service class looks like this:
<?php
namespace Organization\Theme\BasicBundle;
use Organization\Theme\BasicBundle\Entity\Theme;
class ThemeService
{
private $themes = array();
public function register(Theme $theme) {
$name = $theme->getName();
if (in_array($name, array_keys($this->themes))) {
throw new Exception('Unable to register theme, another theme with the same name ('.$name.') is already registered.');
}
$this->themes[$name] = $theme;
}
public function findAll() {
return $this->themes;
}
public function findByName(string $name) {
$result = null;
foreach($this->themes as $theme) {
if ($theme->getName() === $name) {
$result = $theme;
}
}
return $result;
}
}
It's normal that you can't access to the service container, because services are not compiled yet.
To inject tagged services into that bundle, you need to create a new compiler pass.
To create a compiler pass it needs to implements the CompilerPassInterface.
Put the class in the DependencyInjection/Compiler folder of the bundle.
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CustomCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->has('organization.themes')) {
$container->getDefinition('organization.themes')->addMethodCall('register', array(new Organization\Theme\BasicBundle\Entity\Theme(__DIR__)));
}
}
}
Then override the build method of your bundle definition class.
class ThemeBasicBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CustomCompilerPass());
}
}
Some links:
http://symfony.com/doc/current/components/dependency_injection/compilation.html
http://symfony.com/doc/current/cookbook/service_container/compiler_passes.html
http://symfony.com/doc/current/components/dependency_injection/tags.html
Try that it could work :) :
<?php
namespace Organization\Theme\BasicBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class ThemeBasicBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$themes = $container->get('organization.themes');
$themes->register(new Organization\Theme\BasicBundle\Entity\Template(__DIR__));
}
}