How to get matched route name in View - Zend Expressive - php

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.

Related

Not redirected to specific URL in Codeigniter 4

Why is it that whenever I redirect something through the constructor of my Codeigniter 4 controller is not working?
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
if(session('username')){
return redirect()->to('/dashboard');
}
}
public function index()
{
// return view('welcome_message');
}
}
But if I put it inside index it's working as expected.
public function index()
{
if(session('username')){
return redirect()->to('/dashboard');
}
}
The thing is, I do not want to use it directly inside index because I it need on the other method of the same file.
As per the Codeigniter forum, you can no longer use the redirect method in the constructor to redirect to any of the controllers.
Please refer the below link for more information
https://forum.codeigniter.com/thread-74537.html
It clearly states that redirect() will return a class instance instead of setting a header and you cannot return an instance of another class while instantiating a different class in PHP.
So that's why you can't use redirect method in constructor.
Instead, what I can suggest to you is that use the header method and redirect to your desired controller.
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
if(session('username')){
header('Location: /dashboard');
}
}
}
If that's not feasible or difficult to achieve you can follow the below code
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
function __construct()
{
//call to session exists method
$this->is_session_available();
}
private function is_session_available(){
if(session('username')){
return redirect()->to('/dashboard');
}else{
return redirect()->to('/login');
}
}
}
The 2nd solution will be more interactive than the first one. And make sure the method is private. So that it should not be called from other class instances.
The community team has also given a solution to look into the controller filter.
https://codeigniter4.github.io/CodeIgniter4/incoming/filters.html
Please refer to the thread. I hope it may help you in finding a better solution.
In this case you shouldn't even be doing this kind of logic in your controllers. This should be done in a filter and not your controllers.
So you have your controller Register.
You should create a filter in your app/filters folder something like checkLogin.php
That filter should have the following structure:
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class CheckLogin implements FilterInterface
{
/**
* Check loggedIn to redirect page
*/
public function before(RequestInterface $request, $arguments = null)
{
$session = \Config\Services::session();
if (session('username')) {
return redirect()->to('/dashboard');
}
}
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Do something here
}
}
Then in your app/config/Filters.php your should add the filter to the desired controller.
public $aliases = [
'csrf' => \CodeIgniter\Filters\CSRF::class,
'toolbar' => \CodeIgniter\Filters\DebugToolbar::class,
'honeypot' => \CodeIgniter\Filters\Honeypot::class,
'checkLogin' => \App\Filters\CheckLogin::class,
];
// List filter aliases and any before/after uri patterns
public $filters = [
'checkLogin' => ['before' => ['Register']],
];
For more information on filters and how to use then please check the documentation.
https://codeigniter.com/user_guide/incoming/filters.html?highlight=filters
You can then even create filters to your other controllers that would redirect to this one in case the user is not logged in.
Codeigniter 4 using initController() to create constructor.
You can't use redirect() inside __construct() or initController() function.
But you can use $response parameter or $this->response to call redirect in initController() before call another function in controller;
<?php namespace App\Controllers\Web\Auth;
class Register extends \App\Controllers\BaseController
{
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger)
{
// Do Not Edit This Line
parent::initController($request, $response, $logger);
if(session('username')){
$response->redirect(base_url('dashboard')); // or use $this->response->redirect(base_url('dashboard'));
}
}
public function index()
{
// return view('welcome_message');
}
}
I just got into CI 4 and i had the same issue as you did, since i've been approaching the login system as with CI 3.
So here is the proper way of doing it right. Enjoy.
Codeigniter 4 do not has any return on Constructor, but you can use like this
public function __construct()
{
if (!session()->has('user_id')) {
header('location:/home');
exit();
}
}
Don't forget to use exit()
But, you better use Filters

Call component inside component October CMS gives an error

I am trying to extend an existing plugin component and I
need to add a function but use plugins methods.
Here what I have:
<?php namespace Bbrand\Shop\Components;
use Cms\Classes\ComponentBase;
use Jiri\JKShop\Components\Basket;
class Shopextend extends ComponentBase
{
public function componentDetails()
{
return [
'name' => 'shopextend Component',
'description' => 'No description provided yet...'
];
}
public function defineProperties()
{
return [];
}
public function onBasket(){
$data = [];
$data["basket"] = Basket::getSessionBasket();
$data["jkshopSetting"] = \Jiri\JKShop\Models\Settings::instance();
return [
$this->property("idElementWrapperBasketComponent") => $this->renderPartial('#basket-0', $data)
];
}
}
But I'm getting an error
"Non-static method Jiri\JKShop\Components\Basket::getSessionBasket()
should not be called statically" on line 30 of
/Applications/MAMP/htdocs/fidgycube.co/plugins/bbrand/shop/components/Shopextend.php
Any help!?
thanks
You need to add component first.
<?php namespace Bbrand\Shop\Components;
class Shopextend extends ComponentBase
{
public function init()
{
// Add component
$this->addComponent('\Jiri\JKShop\Components\Basket', 'basket', []);
}
}

Slim Framework 3 - using router in a global template

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');

Default value for route params in generated url

I have a site which is localized into several languages. Every public route is prefixed with the locale key (e.g. /{locale}/foo/bar), which gets caught and applied by middleware.
When generating URLs to point to other pages, I end up needing to feed the current locale into every url, like so:
Foo Bar
Otherwise the output url will contain %7Blocale%7D, which breaks it. This strikes me as needlessly verbose. Is there not a way to specify a default value for a given named parameter, such that if no value is explicitly provided for 'locale' it can be defaulted to whatever the current locale is?
I've inspected the UrlGenerator class, but I don't see anything to that effect.
The Route class has a defaults property, but that only appears to be used as part of binding the route to the current request.
Ultimately, not a huge issue, just wondering if anyone has any ideas for ways to save a bit of sanity.
You can use URL defaults as well at a middleware:
use Illuminate\Support\Facades\URL;
URL::defaults(
[
'locale' => $locale
]
);
When you define your routes, use optional variables with defaults:
Routes:
Route::get('{locale?}/foo/bar', 'Controller#fooBar');
Controller:
public function __construct()
{
$this->locale = session()->has('locale') ? session('locale') : 'en';
}
public function fooBar($locale = null)
{
$locale = $locale ?: $this->locale;
}
OR:
public function fooBar($locale = 'en')
{
$locale = $locale ?: $this->locale;
}
Wherever you call your route:
Foo Bar
You could optionally put the constructor in a BaseController class that all your other controllers extend.
There may be better ways to do this, but this would keep you from having to include the locale wherever you call a route.
There isn't any built in means of doing this, but I managed to achieve the desired result by extending the UrlGenerator
<?php
namespace App\Services;
use Illuminate\Routing\UrlGenerator as BaseGenerator;
use Illuminate\Support\Arr;
class UrlGenerator extends BaseGenerator
{
protected $default_parameters = [];
public function setDefaultParameter($key, $value){
$this->default_parameters[$key] = $value;
}
public function removeDefaultParameter($key){
unset($this->default_parameters[$key]);
}
public function getDefaultParameter($key){
return isset($this->default_parameters[$key]) ? $this->default_parameters[$key] : null;
}
protected function replaceRouteParameters($path, array &$parameters)
{
if (count($parameters) || count($this->default_parameters)) {
$path = preg_replace_sub(
'/\{.*?\}/', $parameters, $this->replaceNamedParameters($path, $parameters)
);
}
return trim(preg_replace('/\{.*?\?\}/', '', $path), '/');
}
protected function replaceNamedParameters($path, &$parameters)
{
return preg_replace_callback('/\{(.*?)\??\}/', function ($m) use (&$parameters) {
return isset($parameters[$m[1]]) ? Arr::pull($parameters, $m[1]) : ($this->getDefaultParameter($m[1]) ?: $m[0]);
}, $path);
}
}
Then rebinding our subclass into the service container
class RouteServiceProvider extends ServiceProvider
{
public function register(){
parent::register();
//bind our own UrlGenerator
$this->app['url'] = $this->app->share(function ($app) {
$routes = $app['router']->getRoutes();
$url = new UrlGenerator(
$routes, $app->rebinding(
'request', function ($app, $request) {
$app['url']->setRequest($request);
}
)
);
$url->setSessionResolver(function () {
return $this->app['session'];
});
$app->rebinding('routes', function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
}
//...
}
Then all I needed to do was inject the default locale into the UrlGenerator from the Locale middleware
public function handle($request, Closure $next, $locale = null) {
//...
$this->app['url']->setDefaultParameter('locale', $locale);
return $next($request);
}
Now route('foo.bar') will automatically bind the current locale to the route, unless another is explicitly provided.

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