ZF2 optional route constraints in child routes - php

I'm having an issue with an optional constraint in a route that is non-optional in it's children. My routing structure is as follows:
'profile' => [
'type' => 'segment',
'options' => [
'route' => '/profile[/:id]',
'constraints' => ['id' => '[0-9]*'],
'defaults' => [
'controller' => 'User\Controller\User',
'action' => 'profile'
]
],
'may_terminate' => true,
'child_routes' => [
'sessions' => [
'type' => 'literal',
'options' => [
'route' => '/sessions',
'defaults' => ['action' => 'sessions']
]
]
]
]
Which to my mind should give me the following routes:
/profile - works
/profile/123 - works
/profile/sessions - doesn't work
/profile/123/sessions - works
When I use route 3 in the URL view helper I get the following error:
$this->url('profile/sessions');
Zend\Mvc\Router\Exception\InvalidArgumentException: Missing parameter "id"
I originally had [0-9]+ as my constraint but making it optional (*) doesn't seem to have helped. Has anyone experienced this case before?

Add it to your parent route.
'profile' => [
'type' => 'segment',
'options' => [ // ↓
'route' => '/profile[/:id][/:action]',
'constraints' => [ 'id' => '[0-9]*', 'action' => '[a-zA-Z][a-zA-Z0-9_-]*' ],
'defaults' => [
'controller' => 'User\Controller\User',
'action' => 'profile',
],
],
]
This will make it optional to have id and/or action.
At least in theory it should make all your listed routes possible, there have been some issues with this.

I had the same issue once, the only solution I found was to create a separate route (in your case for /profile/sessions) as the optional parameter for the base route, seems to become obligatory when accessing a child route.

Related

ZF2 routing in links not working correctly

I'm working on a Zend Framework 2 application for work in which I can't seem to route correctly or know where to route it.
I have an Hostname => webapp.foo-bar.com. We decided to add a Subhost => /app/ to the end and the name of this app is called => app. I have a link on a page which it's route would be say => /graph/page-name. But when I hover over the link which looks like:
FooBar
I'd get webapp.foo-bar.com/graph/page-name as opposed to webapp.foo-bar.com/app/graph/page-name.
My Application config is:
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* #link http://github.com/zendframework/ZendSkeletonApplication for the canonical source repository
* #copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
* #license http://framework.zend.com/license/new-bsd New BSD License
*/
return array(
'router' => array(
'routes' => array(
'home' => array(
'type' => 'hostname',
'options' => array(
'route' => 'webapp.foo-bar.com/app',
'defaults' => array(
'__NAMESPACE__' => 'Application\Controller',
'controller' => 'Index',
'action' => 'index',
),
),
),
// The following is a route to simplify getting started creating
// new controllers and actions without needing to create a new
// module. Simply drop new controllers in, and you can access them
// using the path /application/:controller/:action
'application' => array(
'type' => 'Literal',
'options' => array(
'route' => '/application',
'defaults' => array(
'__NAMESPACE__' => 'Application\Controller',
'controller' => 'Index',
'action' => 'index',
),
),
'may_terminate' => true,
'child_routes' => array(
'default' => array(
'type' => 'Segment',
'options' => array(
'route' => '/[:controller[/:action]]',
'constraints' => array(
'controller' => '[a-zA-Z][a-zA-Z0-9_-]*',
'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
),
'defaults' => array(
),
),
),
),
),
),
),
'service_manager' => array(
'abstract_factories' => array(
'Zend\Cache\Service\StorageCacheAbstractServiceFactory',
'Zend\Log\LoggerAbstractServiceFactory',
),
'aliases' => array(
'translator' => 'MvcTranslator',
),
),
'translator' => array(
'locale' => 'en_US',
'translation_file_patterns' => array(
array(
'type' => 'gettext',
'base_dir' => __DIR__ . '/../language',
'pattern' => '%s.mo',
),
),
),
'controllers' => array(
'invokables' => array(
'Application\Controller\Index' => 'Application\Controller\IndexController'
),
),
'view_manager' => array(
'display_not_found_reason' => true,
'display_exceptions' => true,
'doctype' => 'HTML5',
'not_found_template' => 'error/404',
'exception_template' => 'error/index',
'template_map' => array(
'layout/layout' => __DIR__ . '/../view/layout/layout.phtml',
'application/index/index' => __DIR__ . '/../view/application/index/index.phtml',
'error/404' => __DIR__ . '/../view/error/404.phtml',
'error/index' => __DIR__ . '/../view/error/index.phtml',
),
'template_path_stack' => array(
__DIR__ . '/../view',
),
),
// Placeholder for console routes
'console' => array(
'router' => array(
'routes' => array(
),
),
),
);
I saw there is an option like:
FooBar
Would this need to be done on every link, or would we be able to do it in the config level?
Thanks!
Update:
I've managed to get the client to go down using the this->url() method and things are looking fine. Links are working even in the jQuery sections. I am having a small issue in relation to a link going to another controller not showing the action in the link.
<a href="<?php echo $this->url('foo-bar', array('action' => 'bar-foo'))?>?year=2015"</a>
Brings back webapp.foo-bar.com/app/foo-bar?year=2015. I want it to return webapp.foo-bar.com/app/foo-bar/bar-foo?year=2015. Is this a configuration in the Controller of foo-bar or can it be done in this-url()?
Thanks a million for all the help given. Really appriciate it!
If both apps share the same domain, then your configuration is invalid. My version of config file is below.
P.S. I created separate module Sample, so it would not interfere with Application config.
use Zend\Router\Http\Literal;
use Zend\Router\Http\Segment;
use \Sample\Controller\IndexController;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'router' => [
'routes' => [
'v2' => [
'type' => Segment::class,
'options' => [
'route' => '/v2/app',
'defaults' => [
'controller' => IndexController::class,
'action' => 'index',
],
],
'child_routes' => [
'index' => [
'type' => Literal::class,
'options' => [
'route' => '/',
'defaults' => [
'controller' => IndexController::class,
'action' => 'index',
],
],
],
'application' => [
'type' => Segment::class,
'options' => [
'route' => '[/:action]',
'defaults' => [
'controller' => IndexController::class,
'action' => 'index',
],
],
]
],
],
],
],
'controllers' => [
'factories' => [
IndexController::class => InvokableFactory::class,
],
],
'view_manager' => [
'template_path_stack' => [
__DIR__ . '/../view',
],
],
];
This is how you create links to that application:
<?php echo $this->url("v2/application", ['action' => 'page-name']) ?>
In ZF2 a route maps to a specific controller class and its action or method, so they can be of any modules. This route graph/page-name may map to GraphController and its method pageNameAction(), for example. But that can be any other controller and its actions. This depends on your needs.
Now lets come to the type of a route. There are some route plugins in ZF. These can be used to specify a route's type. For example, Hostname specifies domain name and subdomain like subdomain.domain.tld. But you are using domain.tld/v2/app. Hostname type does not allow / in the 'route' => 'subdomain.domain.tld'. So this will not work, in this case.
You can get this type of endpoints (domain.tld/v2/app) without creating any subdomain that you said in your thread. That is why MVC pattern, Framework is more convenient. You can create a lot of endpoints for different purposes(domain.tld/v1/abc domain.tld/v2/mno domain.tld/v3/xyz etc) in your ZF application. They may be mapped to different controllers and actions from different modules.
So you do not need to be dependent on the subdomains as the way you asked.
You also said that you need domain.tld/graph/page-name which actually works as domain.tld/v2/app/graph/page-name. This means you want to hide this part v2/app from the url if I am not wrong. But this is still a challenge.
If you have it graph/page-name used under this v2/app/graph/page-name you may define your route as follows
// Top key of a route
'v2app' => [
'type' => 'Segment',
'options' => [
'route' => '/v2/app',
'defaults' => [
'__NAMESPACE__' => 'Application\Controller',
'controller' => 'Index',
'action' => 'index',
],
],
'may_terminate' => true,
'child_routes' => [
'default' => [
'type' => 'Segment',
'options' => [
'route' => '/',
'defaults' => [
'controller' => 'Application\Controller\Index',
'action' => 'index',
],
],
],
'graph' => [
'type' => 'Segment',
'options' => [
'route' => '[/:controller/:action]',
'defaults' => [
'controller' => 'Application\Controller\Graph',
'action' => 'pageName',
],
],
],
],
],
You can add this to any route section of any module.config.php by correcting the module name, controller name etc. But keep it mind that same configuration for multiple routes would make you unhappy.
Now to output those routes use the following snippet of code
echo $this->url('v2app/graph', [], [], true);
echo $this->url(null, ['controller' => 'graph', 'action' => 'page-name']);
// outputs
// /v2/app/graph/page-name
while v2app is your top route key and graph is the child route of it.
Now you have said about how you should implement links. If you want to show them as menu, you do not have to code like that you said. You can follow this to implement links as menu. But keep it mind providing value (the top key of a route) for the route key while building an array for the navigation.
The following example from above for this route graph/page-name
[
'label' => 'Graph',
// route name not route pattern
'route' => 'graph', // not /graph/page-name
],
Otherwise if you want to show a link in the body of a page you then use $this->url() helper as used above.
Please refer to this DOC for Hostname type.
If we can not help you here please ask this here
Just create a cutomized ViewModel that will contain own url implementation
/**
* #param \Zend\Mvc\MvcEvent $e The MvcEvent instance
* #return void
*/
public function setLayout($e)
{
// Should return your customization
$viewModel = $e->getViewModel();
In that case all your $this->url() will do directly the same trick as:
$this->url('application/default', array('controller' => 'graph', 'action' => 'page-name')
As a partial example to customize render:
$customStrategy = $app->getServiceManager()->get('{Your class name}');
$view->getEventManager()->attach($customStrategy, 1); // 1 - means priority
Inside your class
preg_replace_callback('/<a href=\\"([^\\"]*)\\">(.*)<\\/a>/iU', function ($matches) {
// do whatever you need for each URL
}, $content);
Or it can be used DOM object (but it's relying on your code validity):
$xpath = new \DOMXPath((new \DOMDocument)->loadHTML($content));
foreach ($xpath->query('//a[href]') as $link) {
// ...
}
There is a way around for this purpose. You may try this one. To do that you need to create a custom route plugin and then set that to route pointing to the Graph controller and its target method pageNameAction(), in this case. Please follow the steps below.
Here graph/page-name would work as v2/app/graph/page-name meaning the following link would work as you expect.
FooBar
Please take the custom route plugin from here. Put it under this src/YourModuleName/Route directory using the namespace YourModuleName\Route.
Notice you need to replace 'template_prefix' => 'yourmodulename', with your own module name with all small letters. View templates will be searched for by matching this patten yourmodulename/controller/action-name behind the scene.
'v2app' => [
'type' => 'Segment',
'options' => [
'route' => '[/v2/app]',
'defaults' => [
'controller' => 'YourModuleName\Controller\Index',
'action' => 'index',
],
],
'may_terminate' => true,
'child_routes' => [
'graph' => [
// We call the custom route plugin thus
'type' => 'YourModuleName\Route\AppRoute',
'options' => [
'dir_name' => __DIR__ . '/../view',
'template_prefix' => 'yourmodulename',
'filename_pattern' => '/[a-z0-9_\-]+/',
'defaults' => [
'controller' => 'YourModuleName\Controller\Graph',
'action' => 'pageName',
],
],
],
],
],
Now format the pageNameAction() as the following
public function pageNameAction()
{
// Get path to view template from route params
$pageTemplate = $this->params()->fromRoute('page', null);
// If path is not valid
if($pageTemplate == null) {
$this->getResponse()->setStatusCode(404);
return;
}
$viewModel = new ViewModel();
// Here we set template to be rendered for `v2/app/graph/page-name`
$viewModel->setTemplate($pageTemplate);
return $viewModel;
}
If you need to output link in the view script use this way
echo $this->serverUrl('/graph/page-name');
Hope this would help you!

Zend Framework, PhpRenderer unable to render template adds controllers into the template name

I migrated from ZF2 to ZF3. Now I have a problem. My view script is in the right place, the configuration seems ok, but I get the following error:
Zend\View\Renderer\PhpRenderer::render: Unable to render template "parties/controllers/write-party/add"; resolver could not resolve to a file
which is a fairly common error to solve, but the problem is that for some reason, I get controllers folder in the template path. The template path should be parties/write-party/add.
module.config.php
return [
'controllers' => [
'factories' => [
WritePartyController::class => WritePartyControllerFactory::class,
],
],
'router' => [
'routes' => [
'parties' => [
'type' => Literal::class,
'options' => [
'route' => '/parties',
'defaults' => [
'__NAMESPACE__' => 'Parties\Controllers',
'controller' => 'Index',
'action' => 'index',
],
],
'may_terminate' => true,
'child_routes' => [
'add' => [
'type' => Segment::class,
'options' => [
'route' => '/add',
'defaults' => [
'controller' => WritePartyController::class,
'action' => 'add',
],
],
],
//...
'view_manager' => [
'template_path_stack' => [
'parties' => __DIR__ . '/../view',
],
'strategies' => [
'ViewJsonStrategy',
],
],
],
How to get the correct path to my view script which is parties/write-party/add?
The issue turned out to be more complex than I thought. Internally ZF3 assumes that the folder with the module's controllers is named Controller (singular). If that's not the case, it lets the namespace YourModule/Controllers/ControllersName/ to be transferred to the path your-module/controllers/controllers-name/ and that's what I saw. All this happens in the InjectTemplateListener's mapController method. So I guess it's a bug because things worked for ZF2 without a problem.

zf2 - url view helper: specify parameters for route

My main router goes like this (simplified):
'router' => [
'routes' => [
'blog' => [
'type' => 'regex',
'options' => [
'regex' => "/(?<language>[a-z]{2})?",
'spec' => "/%language%",
'defaults' => [
'controller' => 'Blog\Controller\Posts',
'action' => 'index'
],
],
'may_terminate' => true,
'child_routes' => [
// [...]
'add_post' => [
'type' => 'literal',
'options' => [
'route' => '/admin/post/add',
'defaults' => [
'controller' => 'Blog\Controller\Posts',
'action' => 'add'
]
]
], // end add post
] // end child routes
] // end blog route (main route)
] // end routes
] // end Router
And in the template displayed on "/en/admin/post/add" I have a call to $this->url(), that ends up printing /%language%/admin/post/add.
I have the language code available on $language on my template, and
I'd like to pass it on to url() so it properly constructs the the url using the spec.
Also, I'd like, if possible, not to specify the name of the route on my call to url(), so it uses the default one for $this.
How would I go around to accomplish this?
Thanks and regards
You could use a segment route instead of a regex one and then use
$this->getHelperPluginManager()->getServiceLocator()->get('request')->getUri()->getPath();
in your view to print the actual route it's been used
While #marcosh answer works, since then I've found a simpler solution:
$this->url($this->route, ['language' => $language]);
Will output what I want. Seems clearer to me.

how to create an optional route parameter for zend framework 2 restful route

I'm working with zend framework 2 and I need to create an optional parameter for a route segment that also has two required parameters. Below is a snippet from the module.config.php describing the route. My understanding is that in ZF2 an optional route parameter can be created by using the
[/:param]
which you can see is what I have. It works fine as long as I send the optional param, but when I leave it out the first two params "uname and appname" are appended together into the "uname" constraint. The route is a parent route.
'roles' => array(
'type' => 'segment',
'options' => array(
'route' => '/roles/:uname/:appname[/:locnames]',
'constraints' => array(
'uname' => '[a-zA-Z].+',
'appname' => '[a-zA-Z0-9_-].+',
'locnames' => 'locnames'
),
'defaults' => array(
'controller' => 'Roles/Controller/RolesController'
),
),
),
What am I missing here, I know you can have define optional parameters, but I can't figure out the correct format
Thanks to grizzm0 on #zftalk or helping me with this one. It was a simple regular expressions issue. Removing the dot(.) in the constraints correctly matched the incoming url parameters. So my route now looks like this:
'roles' => array(
'type' => 'segment',
'options' => array(
'route' => '/roles[/:action][/uname/:uname][/appname/:appname][/locnames/:locnames]',
'constraints' => array(
'uname' => '[a-zA-Z]+',
'appname' => '[a-zA-Z0-9_-]+',
'locnames' => 'locnames'
),
'defaults' => array(
'controller' => 'Roles/Controller/RolesController'
),
),
),
'roles' => array(
'type' => 'segment',
'options' => array(
'route' => '/roles[/:action][/uname/:uname][/appname/:appname][/locnames/:locnames]',
'constraints' => array(
'uname' => '[a-zA-Z].+',
'appname' => '[a-zA-Z0-9_-].+',
'locnames' => 'locnames'
),
'defaults' => array(
'controller' => 'Roles/Controller/RolesController'
),
),
),
You can configure your route like this way.
Here inside your roles controller
you have some action live index.
so your route will be
siteurl/roles/index/uname/john/appname/stackexchange/locanames/yourlocanames
here if you don't want to write appname then remove youre paramater so your route will work.

Yii2 route using yii\rest\UrlRule with several parameters

I am trying to use Yii 2 routing for REST API.
Following tutorial at http://www.yiiframework.com/doc-2.0/guide-rest-routing.html, I have already defined (with success) a lot of rule for API entry point like so :
'urlManager' => [
'enablePrettyUrl' => true,
'enableStrictParsing' => true,
'showScriptName' => false,
'rules' => [
[
'class' => 'yii\rest\UrlRule',
'controller' => 'user'
],
],
]
Such a rule defines :
GET /users (list users)
GET /users/123 (show detail of user 123)
Now, my users have games. So I'd like to have urls :
GET /users/123/games (list games of user 123)
GET /users/123/games/864 (details of game 864 for user 123 - such as his scores)
I tried defining my new entry point (withhout success) like so:
'rules' => [
... previous rules ...,
[
'class' => 'yii\rest\UrlRule',
'controller' => [
'game'
],
'tokens' => [
'{userid}' => '<userid:\\d>',
'{gameid}' => '<gameid:\\d>',
],
'patterns' => [
'GET,HEAD /users/{userid}/games' => 'index',
'GET,HEAD /users/{userid}/games/{gameid}' => 'view',
]
]
]
This definition seems wrong because I get a 404 Page not found error.
How should I define my new url routes ?
I would like to use an equivalent format for my definitions, extending 'yii\rest\UrlRule'
I am not sure if this is even possible, the tutorial not mentionning this case.
So I figured out how to use more complex rules.
First, the solution, then the explanation.
Here is the solution:
'rules' => [
... previous rules ...,
[
'class' => 'yii\rest\UrlRule',
'controller' => 'game',
'prefix' => '/users/<userid:\\d+>',
'tokens' => [
'{gameid}' => '<gameid:\\d+>',
],
'patterns' => [
'GET,HEAD' => 'index',
'GET,HEAD {gameid}' => 'view',
]
]
]
And now the explanation:
First my userid / gameid attributes were badly defined. There was a missing "+" after "\d"
The controller seems to be automatically added as a prefix to the patterns. So you have to define both a controller and a prefix (that will be added before the controller).
Parameters in the prefix does not seem to be parsed to find tokens. So I wrote directly the regexp in the prefix instead of adding "userid" as a token.
Finally, there are various "/" automatically added during concatenation of "prefix/controller/pattern" so you don't have to write one.
Do not forget the pluralization rule too ! "game" is singular" but valid urls will be
/users/123/games
/users/123/games/789
Hope it will help.
I think there´s a simple solutions, please try this:
'rules' => [
...
'/users/<userId:\\d+>/games' => 'game/index' ,
'/users/<userId:\\d+>/games/<gameId:\\d+>' => 'game/view' ,
....
];
Just use yii2-nested-rest
It provides REST API for MANY-to-MANY relations in Yii2 framework.
Hope comments will make the magic more understandable:
'rules' => [
// this usual rule for base Yii2 rest usage
['class' => 'yii\rest\UrlRule', 'controller' => ['sitecomponent' ,'sitepage' , 'sitedomain'], 'pluralize'=>false
],
// then rules for yii2-nested-rest
[
// url sitepage/NNN/sitecomponent[/MMM]
// ^^^^^^^^^ ^^^^^^^^^^^^
// resourceName model-endpoint
'class' => 'tunecino\nestedrest\UrlRule',
'modelClass' => 'app\models\SitePage',
'resourceName' => 'sitepage',
'relations' => ['components' => ['sitecomponent'=>'sitecomponent'] ],
// ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^
// relation name url-endpoint controller]
// defined in model SitePage model-endpoint with Actions from nested
],
[
// cross url sitecomponent/NNN/sitepage[/MMM]
'class' => 'tunecino\nestedrest\UrlRule',
'modelClass' => 'app\models\SiteComponent',
'resourceName' => 'sitecomponent',
'relations' => ['pages' => ['sitepage' => 'sitepage'] ],
// ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^
// relation name url-endpoint controller
// from model SiteComponent model-endpoint with Actions from nested
],
],
GET xx.com/v2/publication/12/p/34
[
'class' => 'yii\rest\UrlRule',
'pluralize' => false,//controller是否复数
'controller' => 'v2/publication',//此处一定要加上v2
'tokens' => [
'{id}' => '<id:\\d[\\d,]*>',
'{phase}' => '<phase:\\d[\\d,]*>',
],
// 通过extraPatterns和tokens来实现多个参数传递
'extraPatterns' => [
'GET,HEAD {id}/p/{phase}' => 'phase',
],
],
IN ACTION
public function actionPhase($id, $phase){}

Categories