PHP routing system - php

I'm trying to create a routing system based on annotations (something like on Recess Framework).
<?php
class MyController extends ActionController {
/** !Route GET /hello/$firstname/$lastname **/
public function helloAction($firstname, $lastname) {
echo('Hello '.$firstname.' '.$lastname);
}
}
?>
If I go to http://domain.com/hello/James/Bond I get
Hello James Bond
So I have two questions:
1) Is it a good idea? Pros and cons vs centralized routing system (like Zend Framework). Maybe I don't see problems that my arise later with this routing technique.
2) How to check for duplicate routes if there is regexp in routes
<?php
class MyController extends ActionController {
/**
*!Route GET /test/$id = {
* id: [a-z0-9]
*}
**/
public function testAction($id) {
echo($id);
}
/**
*!Route GET /test/$id = {
* id: [0-9a-z]
*}
**/
public function otherTestAction($id) {
echo($id);
}
}
?>
I get two routes: /test/[a-z0-9]/ and /test/[0-9a-z]/ and if i go to http://domain.com/test/a12/ both routes are valid.
Thanks :)

Try using the Java annotation format which should be much easier to parse uniformly.
It looks something like this:
<?php
class MyController extends ActionController {
/**
#Route(get=/hello/$firstname/$lastname)
#OtherVal(var1=2,var2=foo)
#OtherVal2
**/
public function helloAction($firstname, $lastname) {
echo('Hello '.$firstname.' '.$lastname);
}
}
?>
And parse your annotation out with the following regex:
#(?P<annotation>[A-Za-z0-9_]*)(\((?P<params>[^\)]*))?
And of course cache these where possible to avoid repeated parsing.

Cons:
It may be difficult to keep an overview of URL mappings of all methods in your server.
To change a URL you have to change the source code, mapping is not separated from the application.
If the method signature and mapping are always as related as the example you might use reflection to extract the mapping where helloAction is picked up as /hello and each method argument is a subdirectory of this in the order as they're defined.
Then the annotation wouldn't need to duplicate the URL, only the fact that the method is an endpoint, something like this:
<?php
class MyController extends ActionController {
/** !endpoint(method=GET) **/
public function helloAction($firstname, $lastname) {
echo('Hello '.$firstname.' '.$lastname);
}
}

I think it's a good idea / Decoupling code and entry point seems pretty much used everywhere
Usually you don't check for it: the first one that matches wins.

Doing so is a great idea, as long as you cache the compiled routes in production. There is a cost associated to parsing your files when routing so you want to avoid that when not developing.
As for checking for duplicates, don't check by comparing the declaration. Simply check when routing. If two rules matches, throw a DuplicateRouteException. So, when routing http://domain.com/test/a12/, you'll see that both routes are valid and throw the exception.

Related

Disabling the default routing system with Ubiquity-framework

I'm setting up a small application with Ubiquity Framework in PHP, and I'm trying to disable the default routing system (controller/action/parameters).
The routing system is based on annotations (documented here).
I have a controller with a few routes, which works (don't forget to reset the router cache).
namespace controllers;
class FooController extends ControllerBase
{
/**
* #get("foo")
*/
public function index()
{
echo "I'm on /foo";
}
/**
* #get("bar/{p}")
*/
public function bar($p='default p')
{
echo "I'm on bar/".$p;
}
}
the addresses /foo, /bar and /bar/xxx are accessible, but I would like to disable the default routing system that allows access to the action of an existing controller (without route).
I want to disable the following urls:
/FooController
/FooController/index
/FooController/bar
/FooController/bar/xxx
I didn't find my answer in the doc.
I know the framework is not known (I discovered it through phpbenchmarks website), but the routing system is pretty classic, and it's still php.
If you have any ideas....
Versions:
php 7.3
Ubiquity 2.2.0
I found a solution, indirectly in the doc.
The priority attribute of a route allows you to assign the order in which it will be defined (and therefore requested).
To disable the call of actions on existing controllers, it is therefore possible to define a generic route in the last position returning a 404 error.
namespace controllers;
use Ubiquity\utils\http\UResponse;
class FooController extends ControllerBase
{
...
/**
* #route("{url}","priority"=>-1000)
*/
public function route404($url)
{
UResponse::setResponseCode(404);
echo "Page {$url} not found!";
}
}
If we still want to activate some controllers (the Admin part for example), we must add the requirements attribute, which allows to specify a regex.
namespace controllers;
use Ubiquity\utils\http\UResponse;
class FooController extends ControllerBase
{
...
/**
* #route("{url}","priority"=>-1000,"requirements"=>["url"=>"(?!(a|A)dmin).*?"])
*/
public function route404($url)
{
UResponse::setResponseCode(404);
echo "Page {$url} not found!";
}
}
In this case, the only routes accessible are those defined with annotations + those corresponding to the actions of the Admin controller
With routing problems, routing solution.

The preferred approach to pass variables to blade view

So I'm working on a small CMS in Laravel 5, and the one of the first things that I don't fully get is passing variables to a view.
I have seen things like
return View('view', array('name' => 'your name here'));
Or variables in a view composer
public function compose($view) {
$view->with(Config::get('configfile'));
}
but I'm still wondering if there is a better/more elegant way to do this. For example, with the first approach, I have to give it that array at every view, which just seems like a hassle, and with the view composers, it just feels like there should be a better solution.
Is there a recommended way to push these variables on the the view?
Also, I'm talking about a set of variables that are needed in every view. for example the name and the slogan of the website.
Thanks.
Have you considered view()->share see the docs Sharing Data with All Views
<?php
namespace App\Providers;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
view()->share('key', 'value');
}
}
it allows you to share a piece to data with all your views. I don't see any reason why you couldn't load all your config in this way if your needing to access it on the majority of views.
You can use Route Model Binding: https://laravel.com/docs/5.2/routing#route-model-binding for model specific data binding. Other than that, there's no (practical) way to do it.

Why we use "Action" in Symfony2 controller's methods?

I've just started working my way through the symfony2 book.
I wonder why do we named our controller's functions Action:
public function [something]Action() { // ...
In everyone example in the book thus far and all code I see online Action is the function name. There's any reason for it?
This works perfectly:
<?php
// src/AppBundle/Controller/LuckyController.php
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
class LuckyController extends Controller{
/**
* #Route("/lucky/number/{count}")
*/
public function countTESTING($count){
return new Response(
'<html><body>I DONT HAVE TO CALL THIS somethingACTION</body></html>
');
}
}
?>
I've tried googline this but I see no mention or reasoning as to why. Could someone explain why we use that suffix?
It's just a conventions. You can use those suffixes, but you can also do without it.
If you have
public function somethingAction()
in your controller, you can refer it in the routing configuration in this way:
index:
path: /path_for_something
defaults: { _controller: AppBundle:Index:something }
The _controller parameter uses a simple string pattern called the logical controller name. So, AppBundle:Index:something means:
Bundle: AppBundle
Controller class: IndexController
Method name: somethingAction
But, you can also do this without this feature. Symfony is very flexible, and it does not force you to do almost anything. It is just one of many ways you have to do the same thing.
If you adopt this convention, it's easier for you to understand which action do you have in your controller, it's easy for other developer to understand your code, and it's easier for symfony2 to locate your actions/controllers inside your bundle, so that you can also overriding controllers. This is the best practice.
But if you don't want these benefits, you can using its fully-qualified class name and method as well:
index:
path: /something
defaults: { _controller: AppBundle\Controller\IndexController::indexAction }
But, as the documentation say:
if you follow some simple conventions, the logical name is more
concise and allows more flexibility.
No it wasn't just a naming convention. It was used to execute some code before or after every controller 'action' method. Like checking is user has logged in.
It is based on magic __call function which is executed for a non-existent or non-public method call.
$controller = new Posts();
$controller->index();
class Posts
{
public function __call($name, $args)
{
//run code before
call_user_func_array()[$this, "$nameAction"], $args);
//run code after
}
public function indexAction()
{
}
}
You HAVE TO name your actions
public function somethingAction(){}
because your routes point to a controller, and the action you want to call.
you can also have private functions in your controller, that you will only name
private function something(){}
I say that using yml to configure controllers, i dont believe its different when using annotations, but my advise is to use yml for configuring controllers... really !

How to prevent Symfony profiler from accessing or executing a listener

My user has countTasks property, with corresponding setter and getter:
class User implements UserInterface, \Serializable
{
/**
* #var integer
*/
private $countTasks;
}
I want this property to be always shown in the application's navigation bar (the "14" number in red):
Obviously, this property should be set for every controller. (Actually, only for every that deals with rendering the navigation bar, but that's not the case here). So the application should count tasks for the currently logged-in user for every controller.
I found a relevant topic in the Symfony cookbook: How to Setup before and after Filters, and I managed to implement it:
Acme\TestBundle\EventListener\UserListener.php:
namespace Acme\TestBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class UserListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ( ! is_array($controller)) {
return;
}
$securityContext = $controller[0]->get('security.context');
// now count tasks, but only if a user logged-in
if ($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') or $securityContext->isGranted('IS_AUTHENTICATED_FULLY'))
{
$user = $securityContext->getToken()->getUser();
// ...
// countig tasks and setting $countTasks var
// ...
$user->setCountTasks($countTasks);
}
}
}
services.yml:
services:
acme.user.before_controller:
class: Acme\TestBundle\EventListener\UserListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
It works as expected and I'm able to pull the property in a Twig template like this:
{{ app.user.countTasks }}
It works as expected in prod env.
In dev however, profiler throws UndefinedMethodException:
UndefinedMethodException: Attempted to call method "get" on class "Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController" in ...\src\Acme\TestBundle\EventListener\UserListener.php line 18.
where line 18 is this one:
$securityContext = $controller[0]->get('security.context');
As a quick patch I added additional check (before line 18) to prevent profiler from executing the further logic:
if (is_a($controller[0], '\Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController'))
{
return;
}
$securityContext = $controller[0]->get('security.context');
and it has made the trick. But I'm afraid it's not the right way. I'm also afraid that I'm loosing some part of debug information in profiler.
Am I right with my concerns? Can you point me to a better way to prevent profiler from executing this listener? In config somehow?
Even in Symfony's documentation How to Setup before and after Filters, an instanceof condition is being evaluated in line 29.
I'd go about saying that if the doc's doing it, you're pretty safe doing it yourself (unless stated otherwise, which is not the case).
In the beginning I was trying to fix the issue conversely than I should. During testing it turned out that I have to exclude some other core controllers too. Of course, rather than block unwanted controllers I should have allowed the required ones only.
I ended up creating an empty interface UserTasksAwareController:
namespace Acme\TestBundle\Controller;
interface UserTasksAwareController
{}
fixing that validity check in UserListener.php:
if ( ! $controllers[0] instanceof UserTasksAwareController) {
return;
}
and implementing it in every other controller which deals with displaying countTasks property, like this one:
class UserController extends Controller implements UserTasksAwareController
So, the problem I had was just another one when you forget to program to an interface, not an implementation.

Extend Laravels Router class (4.1)

I would like to extend Laravels Router class (Illuminate\Routing\Router) to add a method I need a lot in my application.
But sadly I can't get this to work. I already extended other classes successfully so I really have no idea where my wrong thinking comes from.
Anyway, right into the code:
<?php
namespace MyApp\Extensions;
use Illuminate\Routing\Router as IlluminateRouter;
class Router extends IlluminateRouter
{
public function test()
{
$route = $this->getCurrentRoute();
return $route->getParameter('test');
}
}
So as you see I want to get the parameter set by {test} in routes.php with a simple call like:
Router::test();
Not sure how to go on now. Tried to bind it to the IOC-Container within my ServiceProvider in register() and boot() but I got no luck.
Whatever I try I get either a constructor error or something else.
All solutions I found are too old and the API has changed since then.
Please help me!
edit:
I already tried binding my own Router within register() and boot() (as said above) but it doesn't work.
Here is my code:
<?php
namespace MyApp;
use Illuminate\Support\ServiceProvider;
use MyApp\Extensions\Router;
class MyAppServiceProvider extends ServiceProvider {
public function register()
{
$this->app['router'] = $this->app->share(function($app)
{
return new Router(new Illuminate\Events\Dispatcher);
}
// Other bindings ...
}
}
When I try to use my Router now I have the problem that it needs an Dispatcher.
So I have to do:
$router = new Router(new Illuminate\Events\Dispatcher); // Else I get an exception :(
Also it simply does nothing, if I call:
$router->test();
:(
And if I call
dd($router->test());
I get NULL
Look at: app/config/app.php and in the aliases array. You will see Route is an alias for the illuminate router via a facade class.
If you look at the facade class in Support/Facades/Route.php of illuminate source, you will see that it uses $app['router'].
Unlike a lot of service providers in laravel, the router is hard coded and cannot be swapped out without a lot of work rewiring laravel or editing the vendor source (both are not a good idea). You can see its hardcoded by going to Illuminate / Foundation / Application.php and searching for RoutingServiceProvider.
However, there's no reason i can think of that would stop you overriding the router class in a service provider. So if you create a service provider for your custom router, which binds to $app['router'], that should replace the default router with your own router.
I wouldn't expect any issues to arise from this method, as the providers should be loaded before any routing is done. So overriding the router, should happen before laravel starts to use the router class, but i've not this before, so be prepared for a bit of debugging if it doesn't work straight away.
So I was asking in the official Laravel IRC and it seems like you simply can't extend Router in 4.1 anymore. At least that's all I got as a response in a pretty long dialogue.
It worked in Laravel 4.0, but now it doesn't. Oh well, maybe it will work in 4.2 again.
Other packages suffer from this as well: https://github.com/jasonlewis/enhanced-router/issues/16
Anyway, personally I'll stick with my extended Request then. It's not that much of a difference, just that Router would've been more dynamic and better fitting.
I'm using Laravel 4.2, and the router is really hard coded into the Application, but I extended it this way:
Edit bootstrap/start.php, change Illuminate\Foundation\Application for YourNamespace\Application.
Create a class named YourNamespace\Application and extend \Illuminate\Foundation\Application.
class Application extends \Illuminate\Foundation\Application {
/**
* Register the routing service provider.
*
* #return void
*/
protected function registerRoutingProvider()
{
$this->register(new RoutingServiceProvider($this));
}
}
Create a class named YourNamespace\RoutingServiceProvider and extend \Illuminate\Routing\RoutingServiceProvider.
class RoutingServiceProvider extends \Illuminate\Routing\RoutingServiceProvider {
protected function registerRouter()
{
$this->app['router'] = $this->app->share(function($app)
{
$router = new Router($app['events'], $app);
// If the current application environment is "testing", we will disable the
// routing filters, since they can be tested independently of the routes
// and just get in the way of our typical controller testing concerns.
if ($app['env'] == 'testing')
{
$router->disableFilters();
}
return $router;
});
}
}
Finally, create YourNamespace\Router extending \Illuminate\Routing\Router and you're done.
NOTE: Although you're not changing the name of the class, like Router and RoutingServiceProvider, it will work because of the namespace resolution that will point it to YourNamespace\Router and so on.

Categories