Slim Framework 3 - using router in a global template - php

I'm relatively new to Slim Framework 3. One thing I'm trying to understand is how to use the router, $this->router, in a "global" template.
What I mean by this is a template such as a navigation menu - something that appears on every page.
For templates I'm using the "php-view" library as per the example tutorial which I installed with:
composer require slim/php-view
In my templates directory I have a file called nav.php where I want to output my links.
I understand how to call the router like so
Sign Up
But... the example tutorial only shows how you would pass that link from 1 individual place, e.g. $app->get('/sign-up' ... })->setName("sign-up");
How can you use the router globally in any template, without passing it into every individual URL route as a parameter?
I'm more familiar with frameworks like CakePHP where there is an "AppController" which allows you to set things globally, i.e. available in every request. I don't know if this is how it's done in Slim but this is the effect I'm after.

Well, you can pass it as template variable.
When you instantiate or register PhpRenderer in a container, you have multiple options to define a "global" variable, i.e. a variable that is accessible in all of your templates:
// via the constructor
$templateVariables = [
"router" => "Title"
];
$phpView = new PhpRenderer("./path/to/templates", $templateVariables);
// or setter
$phpView->setAttributes($templateVariables);
// or individually
$phpView->addAttribute($key, $value);
Assuming you're registering PhpRenderer via Pimple:
<?php
// Create application instance
$app = new \Slim\App();
// Get container
$container = $app->getContainer();
// Register PhpRenderer in the container
$container['view'] = function ($container) {
// Declaring "global" variables
$templateVariables = [
'router' => $container->get('router')
];
// And passing the array as second argument to the contructor
return new \Slim\Views\PhpRenderer('path/to/templates/with/trailing/slash/', $templateVariables);
};

<?php namespace App\Helpers;
/********************/
//LinksHelper.php
/********************/
use Interop\Container\ContainerInterface;
class LinksHelper
{
protected $ci;
public function __construct(ContainerInterface $container){
$this->ci = $container;
}
public function __get($property){
if ($this->ci->has($property)) {
return $this->ci->get($property);
}
}
public function pathFor($name, $data = [], $queryParams = [], $appName = 'default')
{
return $this->router->pathFor($name, $data, $queryParams);
}
public function baseUrl()
{
if (is_string($this->uri)) {
return $this->uri;
}
if (method_exists($this->uri, 'getBaseUrl')) {
return $this->uri->getBaseUrl();
}
}
public function isCurrentPath($name, $data = [])
{
return $this->router->pathFor($name, $data) === $this->uri->getPath();
}
public function setBaseUrl($baseUrl)
{
$this->uri = $baseUrl;
}
}
?>
<?php
/********************/
//dependencies.php
/********************/
$container['link'] = function ($c) {
return new \App\Helpers\LinksHelper($c);
};
// view renderer
$container['view'] = function ($c) {
$settings = $c->get('settings');
$view = new App\Views\MyPhpRenderer($settings['renderer']['template_path']);
$view->setLayout('default.php');
//$view->addAttribute('title_for_layout', $settings['title_app'] .' :: ');
$view->setAttributes([
'title_for_layout'=>$settings['title_app'] .' :: ',
'link' => $c->get('link')
]);
return $view;
};
?>
<?php
/********************/
//routes.php
/********************/
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
$app->get('/', function (Request $request, Response $response, array $args) {
return $this->view->render($response, 'your_view.php');
})->setName('home');
?>
<?php
/********************/
//your_view.php
/********************/
?>
Home

You should create a new class, e.g. MainMenu, and there you should create an array with all paths for menu. Object of MainMenu should return an array with labels and paths and then you can pass that array to your view:
$menu = (new MainMenu())->buildMenu();
$response = $this->view->render($response, "index.phtml", [
'menu' => $menu
]);
Then in your *.phtml file you have access to the $menu variable. But what if you do not want repeat that code in each route?
Use middlewares. You can pass a variable from middleware using
$request = $request->withAttribute('foo', 'bar');
and retrieve
$foo = $request->getAttribute('foo');

Related

Overriding injected class from routing group in Slim 4?

I have a Slim4 Application composed of several modules separated in different routing groups, like so:
$app->group('/app', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleWare::class);
$app->group('/api', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleware::class);
$app->group('/admin', function(RouteCollectorProxy $app) {
/*blah blah*/
})->add(MyMiddleware::class);
MyMiddleware receives an Interface
class MyMiddleware
{
public function __construct(IMyInterface $myServiceImplementingInterface) { /*blah blah*/ }
}
When we setup the container, we tell it which class to inject so PHP-DI know which class to construct the middleware with:
/* bootstraping */
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions(__DIR__ . '/container.php');
$container = $containerBuilder->build();
and
/*container.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new MyServiceImplementingInterface();
},
];
My main question is:
Would it be possible to somehow override the implementation of the container setup for IMyInterface::class based on the Routing Group ? so I could have something like:
Main container setup:
/*container.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new MyServiceImplementingInterface();
},
];
Specific route group container setup:
/*container.admin.php*/
return [
IMyInterface::class => function (ContainerInterface $container) {
return new AnotherServiceImplementingInterface();
},
];
I suggest using two different objects of MyMiddleware class for different groups, each constructed using appropriate implementation of IMyInterface. You can tell PHP-DI to call the constructor with the parameters you want.
Here I created two instances of MyMiddleware, one with the name AdminMiddleware and the other named ApiMiddleware in the container. using DI\create()->constructor() method, I configure the DI to inject different implementations of IMyInterface while building these two objects:
<?php
use DI\ContainerBuilder;
use Slim\Factory\AppFactory;
// this is the path of autoload.php relative to my index.php file
// change it according to your directory structure
require __DIR__ . '/../vendor/autoload.php';
interface IMyInterface {
public function sampleMethod();
}
class MyServiceImplementingInterface implements IMyInterface {
public function sampleMethod() {
return 'This implementation is supposed to be used for API endpoint middleware';
}
}
class AnotherServiceImplementingInterface implements IMyInterface {
public function sampleMethod() {
return 'This implementation is supposed to be used for Admin middleware';
}
}
class MyMiddleware
{
private $service;
public function __construct(IMyInterface $myServiceImplementingInterface) {
$this->service = $myServiceImplementingInterface;
}
public function __invoke($request, $handler)
{
$response = $handler->handle($request);
$response->getBody()->write($this->service->sampleMethod());
return $response;
}
}
$containerBuilder = new ContainerBuilder();
$containerBuilder->addDefinitions([
'AdminMiddleware' => DI\create(MyMiddleware::class)->constructor(DI\get(AnotherServiceImplementingInterface::class)),
'ApiMiddleware' => DI\create(MyMiddleware::class)->constructor(DI\get(MyServiceImplementingInterface::class))
]);
$container = $containerBuilder->build();
AppFactory::setContainer($container);
$app = AppFactory::create();
$app->group('/admin', function($app) {
$app->get('/dashboard', function($request, $response, $args){
return $response;
});
})->add($container->get('AdminMiddleware'));
$app->group('/api', function($app) {
$app->get('/endpoint', function($request, $response, $args){
return $response;
});
})->add($container->get('ApiMiddleware'));
$app->run();

repository pattern in laravel doesn't seem to work

App::bind('App\Http\Repositories\languageRepository',
function( $app, array $parameters)
{
return new App\Http\Repositories\languageRepository($parameters[0]);
} );
Route::get('/test/{id}', 'testController#getme');
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Repositories\languageRepository;
class test extends Controller
{
//
protected $language;
public function __construct(languageRepository $rep){
$this->language = $rep;
}
public function getme(){
$this->language->getMe();
}
}
When user accesses the route /test/5 for example, it goes to test Controller. what I'd like to do is that it should automatically pass my route parameter to App:bind function and automatically create languageRepository class with the constructor value passed as my route paramter. what happens is the code actually tells me $parameters[0] is undefined offset. why is that? I've tried App::make but then how do I pass the parameter from route to App::make?
You can accomplish this using the container's request instance, for query parameters:
App::bind('App\Http\Repositories\languageRepository',function($app)
{
$request = $app['request'];
$parameters = $request->all();
return new App\Http\Repositories\languageRepository($parameters[0]);
});
You can accomplish this using the container's request instance, for a route parameter:
App::bind('App\Http\Repositories\languageRepository',function($app)
{
$request = $app['request'];
$segment = $request->segment(1);
return new App\Http\Repositories\languageRepository($segment);
});

phpSlim twg-view not working

i am getting error in my slim framework application. i don't know why twig-view is not working. twig-view is downloaded in vendor directory.
this is my index file
<?php
require __DIR__ . '/vendor/autoload.php';
// Settings
$config = [
'settings' => [
'displayErrorDetails' => true,
'addContentLengthHeader' => false,
],
];
$app = new \Slim\App($config);
// Get container
$container = $app->getContainer();
// Register component on container
$container['view'] = function ($container) {
$view = new \Slim\Views\Twig( __DIR__ . '/resources/views', [
'cache' => false
]);
// Instantiate and add Slim specific extension
$view->addExtension(new Slim\Views\TwigExtension(
$container['router'],
$container['request']->getUri()
));
return $view;
};
// Home
$app->get('/home','index');
function index($request, $response, $args)
{
return $this->view->render($response, 'home.twig'); // here is the error
}
$app->run();
i am getting error om $this keyword
error details
Details
Type: Error
Message: Using $this when not in object context
File: C:\xampp\htdocs\slim\api\index.php
Line: 42
It is not possible to use this when you do not have a closure
If you use a Closure instance as the route callback, the closure’s state is bound to the Container instance. This means you will have access to the DI container instance inside of the Closure via the $this keyword.
(Reference: http://www.slimframework.com/docs/objects/router.html)
You can separate it when you assign the closure to a variable
$indexRoute = function ($request, $response, $args)
{
return $this->view->render($response, 'home.twig'); // here is the error
}
$app->get('/home', $indexRoute);
You are declaring route incorectly, try
// This callback will process GET request to /index URL
$app->get('/index', function($request, $response, $args) {
return $this->view->render($response, 'home.twig');
});
Instead of declaring a function, you should call $app method to register a route.
EDIT
It is also possible to "separate" route declaration from the callback. You can create separate classes (a-la controllers in MVC pattern), like this:
// Declaring a controller class with __invoke method, so it acts as a function
class MyController
{
public function __invoke($request, $resposne)
{
// process a request, return response
}
}
// And here's how you add it to the route
$app->get('/index', 'MyController');
I suggest you read the appropriate section of the documentation. It's dead simple.

How to get matched route name in View - Zend Expressive

I know that I can generate URL passing the route name
<?php echo $this->url('route-name') #in view file ?>
But can I get information in opposite direction?
From current URL/URI, I need to get route name.
Real case is: I have layout.phtml where is the top menu (html).
Current link in the menu need to be marked with css class. So, example what I need is:
<?php // in layout.phtml file
$index_css = $this->getRouteName() == 'home-page' ? 'active' : 'none';
$about_css = $this->getRouteName() == 'about' ? 'active' : 'none';
$contact_css = $this->getRouteName() == 'contact' ? 'active' : 'none';
?>
I am using fast route, but I am interesting in any solution. Solution doesn't have to be in View file.
From my research, there is such information in RouteResult instance in the public method getMatchedRouteName(). The problem is how to reach to this instance from the View.
We know that we can get RouteResult, but from the Request object which is in a Middleware's __invoke() method.
public function __invoke($request, $response, $next){
# instance of RouteResult
$routeResult = $request->getAttribute('Zend\Expressive\Router\RouteResult');
$routeName = $routeResult->getMatchedRouteName();
// ...
}
As #timdev recommended we will find inspiration in existing helper UrlHelper and make almost the same implementation in custom View Helper.
In short we will create 2 classes.
CurrentUrlHelper with method setRouteResult() and
CurrentUrlMiddleware with __invoke($req, $res, $next)
We will inject the CurrentUrlHelper in CurrentUrlMiddleware and
in __invoke() method call the CurrentUrlHelper::setRouteResult() with appropriate RouteResult instance.
Later we can use our CurrentUrlHelper with RouteResult instance in it. Both classes should have an Factory too.
class CurrentUrlMiddlewareFactory {
public function __invoke(ContainerInterface $container) {
return new CurrentUrlMiddleware(
$container->get(CurrentUrlHelper::class)
);
}
}
class CurrentUrlMiddleware {
private $currentUrlHelper;
public function __construct(CurrentUrlHelper $currentUrlHelper) {
$this->currentUrlHelper = $currentUrlHelper;
}
public function __invoke($request, $response, $next = null) {
$result = $request->getAttribute('Zend\Expressive\Router\RouteResult');
$this->currentUrlHelper->setRouteResult($result);
return $next($request, $response); # continue with execution
}
}
And our new helper:
class CurrentUrlHelper {
private $routeResult;
public function __invoke($name) {
return $this->routeResult->getMatchedRouteName() === $name;
}
public function setRouteResult(RouteResult $result) {
$this->routeResult = $result;
}
}
class CurrentUrlHelperFactory{
public function __invoke(ContainerInterface $container){
# pull out CurrentUrlHelper from container!
return $container->get(CurrentUrlHelper::class);
}
}
Now we only need to register our new View Helper and Middleware in the configs:
dependencies.global.php
'dependencies' => [
'invokables' => [
# dont have any constructor!
CurrentUrlHelper::class => CurrentUrlHelper::class,
],
]
middleware-pipeline.global.php
'factories' => [
CurrentUrlMiddleware::class => CurrentUrlMiddlewareFactory::class,
],
'middleware' => [
Zend\Expressive\Container\ApplicationFactory::ROUTING_MIDDLEWARE,
Zend\Expressive\Helper\UrlHelperMiddleware::class,
CurrentUrlMiddleware::class, # Our new Middleware
Zend\Expressive\Container\ApplicationFactory::DISPATCH_MIDDLEWARE,
],
And finaly we can register our View Helper in templates.global.php
'view_helpers' => [
'factories' => [
# use factory to grab an instance of CurrentUrlHelper
'currentRoute' => CurrentUrlHelperFactory::class
]
],
it's important to register our middleware after ROUTING_MIDDLEWARE and before DISPATCH_MIDDLEWARE!
Also, we have CurrentUrlHelperFactory only to assign it to the key 'currentRoute'.
Now you can use helper in any template file :)
<?php // in layout.phtml file
$index_css = $this->currentRoute('home-page') ? 'active' : 'none';
$about_css = $this->currentRoute('about') ? 'active' : 'none';
$contact_css = $this->currentRoute('contact') ? 'active' : 'none';
?>
As you note in your self-answer, UrlHelper is a useful thing to notice. However, creating a new helper that depends on UrlHelper (and reflection) isn't ideal.
You're better off writing your own helper, inspired UrlHelper but not dependent on it.
You can look at the code for UrlHelper, UrlHelperFactory and UrlHelperMiddleware to inform your own implementation.
You could wrap the template renderer in another class and pass the Request to there, subtract what you need and inject it into the real template renderer.
Action middleware:
class Dashboard implements MiddlewareInterface
{
private $responseRenderer;
public function __construct(ResponseRenderer $responseRenderer)
{
$this->responseRenderer = $responseRenderer;
}
public function __invoke(Request $request, Response $response, callable $out = null) : Response
{
return $this->responseRenderer->render($request, $response, 'common::dashboard');
}
}
The new wrapper class:
<?php
declare(strict_types = 1);
namespace Infrastructure\View;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Diactoros\Stream;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Template\TemplateRendererInterface;
class ResponseRenderer
{
private $templateRenderer;
public function __construct(TemplateRendererInterface $templateRenderer)
{
$this->templateRenderer = $templateRenderer;
}
public function render(Request $request, Response $response, string $templateName, array $data = []) : Response
{
$routeResult = $request->getAttribute(RouteResult::class);
$data['routeName'] = $routeResult->getMatchedRouteName();
$body = new Stream('php://temp', 'wb+');
$body->write($this->templateRenderer->render($templateName, $data));
$body->rewind();
return $response->withBody($body);
}
}
Code is borrowed from GitHub.

thephpleague route - locale in uri

Hi I've switched to thephpleague route from a self written routingengine. My question is: Can i access wildcard variables outside of the route action method?
Example
Routing Part:
$router = new League\Route\RouteCollection;
$router->addRoute('GET', '{locale}/{controller}/{action}', '\Backend\Controller\{controller}Controller::{action}');
$dispatcher = $router->getDispatcher();
//making a call with, for example, '/en/foo/bar', or '/de/foo/bar'
$response = $dispatcher->dispatch($oRequest->getMethod(), $oRequest->getPathInfo());
$response->send();
Controller part
class FooController extends AppController {
public function __construct() {
//<---- here i want to access the {locale} from the URI somehow
}
public function bar(Request $request, Response $response, array $args) {
// $args = [
// 'locale' => 'de', // the actual value of {locale}
// 'controller' => 'foo' // the actual value of {controller}
// 'action' => 'bar' // the actual value of {bar}
// ];
}
}
I could not find anything in the docs route.thephpleague
I'm using "league/route": "^1.2"
I think by default, you can only call methods in controller classes statically and when you do that the controller's constructor will not be called automatically. And also you can't use the route's wildcards to dynamically call controllers.
Please take note that this is not secure, but you should still be able to do what you want to happen with a Custom Strategy in league/route like this:
Controller
class TestController {
public function __construct($args) {
//the wildcards will be passed as an array in the constructor like this
$this->locale = $args['locale'];
}
public function check(Request $request, Response $response, array $args) {
// $args = [
// 'locale' => 'de', // the actual value of {locale}
// 'controller' => 'Test' // the actual value of {controller}
// 'action' => 'check' // the actual value of {action}
// ];
return $response;
}
}
Custom Strategy
class CustomStrategy implements StrategyInterface {
public function dispatch($controller, array $vars)
{
$controller_parts = [];
foreach($controller as $con){
foreach ($vars as $key => $value) {
$placeholder = sprintf('{%s}', $key);
$con = str_replace($placeholder, $value, $con);
}
$controller_parts[] = $con;
}
//the controller will be instantiated inside the strategy
$controllerObject = new $controller_parts[0]($vars);
//and the action will be called here
return $controllerObject->$controller_parts[1](Request::createFromGlobals(),new Response(),$vars);
}
}
Routing with custom strategy integration
$router = new League\Route\RouteCollection;
$router->setStrategy(new CustomStrategy()); //integrate the custom strategy
$router->addRoute('GET', '/{locale}/{controller}/{action}', '{controller}Controller::{action}');
$dispatcher = $router->getDispatcher();
//if your url is /en/Test/check, then TestController->check() will be called
$response = $dispatcher->dispatch($oRequest->getMethod(), $oRequest->getPathInfo());
$response->send();

Categories