I followed the following article to setup laravel with graphql on my local machine:
https://auth0.com/blog/developing-and-securing-graphql-apis-with-laravel
I following the complete article step by step without any issue. But when I run my app using
php artisan serve
and hit the endpoint localhost:8000/graphql, I get the following error.
[Sat Aug 31 23:09:20 2019] PHP Fatal error: Declaration of App\GraphQL\Queries\WineQuery::type() must be compatible with
Rebing\GraphQL\Support\Field::type(): GraphQL\Type\Definition\Type
in /Users/sushilsingh/Projects/winestore/app/GraphQL/Queries/WineQuery.php on line 9
Here is my WineQuery.php
<?php
namespace App\GraphQL\Queries;
use App\Wine;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Query;
class WineQuery extends Query
{
protected $attributes = [
'name' => 'wine',
];
public function type()
{
return GraphQL::type('Wine');
}
public function args()
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
],
];
}
public function resolve($root, $args)
{
return Wine::findOrFail($args['id']);
}
}
Here is graphql.php
<?php
declare(strict_types=1);
use example\Type\ExampleType;
use example\Query\ExampleQuery;
use example\Mutation\ExampleMutation;
use example\Type\ExampleRelationType;
return [
// The prefix for routes
'prefix' => 'graphql',
// The routes to make GraphQL request. Either a string that will apply
// to both query and mutation or an array containing the key 'query' and/or
// 'mutation' with the according Route
//
// Example:
//
// Same route for both query and mutation
//
// 'routes' => 'path/to/query/{graphql_schema?}',
//
// or define each route
//
// 'routes' => [
// 'query' => 'query/{graphql_schema?}',
// 'mutation' => 'mutation/{graphql_schema?}',
// ]
//
'routes' => '{graphql_schema?}',
// The controller to use in GraphQL request. Either a string that will apply
// to both query and mutation or an array containing the key 'query' and/or
// 'mutation' with the according Controller and method
//
// Example:
//
// 'controllers' => [
// 'query' => '\Rebing\GraphQL\GraphQLController#query',
// 'mutation' => '\Rebing\GraphQL\GraphQLController#mutation'
// ]
//
'controllers' => \Rebing\GraphQL\GraphQLController::class.'#query',
// Any middleware for the graphql route group
'middleware' => [],
// Additional route group attributes
//
// Example:
//
// 'route_group_attributes' => ['guard' => 'api']
//
'route_group_attributes' => [],
// The name of the default schema used when no argument is provided
// to GraphQL::schema() or when the route is used without the graphql_schema
// parameter.
'default_schema' => 'default',
// The schemas for query and/or mutation. It expects an array of schemas to provide
// both the 'query' fields and the 'mutation' fields.
//
// You can also provide a middleware that will only apply to the given schema
//
// Example:
//
// 'schema' => 'default',
//
// 'schemas' => [
// 'default' => [
// 'query' => [
// 'users' => 'App\GraphQL\Query\UsersQuery'
// ],
// 'mutation' => [
//
// ]
// ],
// 'user' => [
// 'query' => [
// 'profile' => 'App\GraphQL\Query\ProfileQuery'
// ],
// 'mutation' => [
//
// ],
// 'middleware' => ['auth'],
// ],
// 'user/me' => [
// 'query' => [
// 'profile' => 'App\GraphQL\Query\MyProfileQuery'
// ],
// 'mutation' => [
//
// ],
// 'middleware' => ['auth'],
// ],
// ]
//
// 'schemas' => [
// 'default' => [
// 'query' => [
// // 'example_query' => ExampleQuery::class,
// ],
// 'mutation' => [
// // 'example_mutation' => ExampleMutation::class,
// ],
// 'middleware' => [],
// 'method' => ['get', 'post'],
// ],
// ],
'schemas' => [
'default' => [
'query' => [
'wine' => App\GraphQL\Queries\WineQuery::class,
'wines' => App\GraphQL\Queries\WinesQuery::class,
]
],
],
// The types available in the application. You can then access it from the
// facade like this: GraphQL::type('user')
//
// Example:
//
// 'types' => [
// 'user' => 'App\GraphQL\Type\UserType'
// ]
//
'types' => [
// 'example' => ExampleType::class,
// 'relation_example' => ExampleRelationType::class,
// \Rebing\GraphQL\Support\UploadType::class,
'Wine' => App\GraphQL\Types\WineType::class,
],
// The types will be loaded on demand. Default is to load all types on each request
// Can increase performance on schemes with many types
// Presupposes the config type key to match the type class name property
'lazyload_types' => false,
// This callable will be passed the Error object for each errors GraphQL catch.
// The method should return an array representing the error.
// Typically:
// [
// 'message' => '',
// 'locations' => []
// ]
'error_formatter' => ['\Rebing\GraphQL\GraphQL', 'formatError'],
/*
* Custom Error Handling
*
* Expected handler signature is: function (array $errors, callable $formatter): array
*
* The default handler will pass exceptions to laravel Error Handling mechanism
*/
'errors_handler' => ['\Rebing\GraphQL\GraphQL', 'handleErrors'],
// You can set the key, which will be used to retrieve the dynamic variables
'params_key' => 'variables',
/*
* Options to limit the query complexity and depth. See the doc
* # https://github.com/webonyx/graphql-php#security
* for details. Disabled by default.
*/
'security' => [
'query_max_complexity' => null,
'query_max_depth' => null,
'disable_introspection' => false,
],
/*
* You can define your own pagination type.
* Reference \Rebing\GraphQL\Support\PaginationType::class
*/
'pagination_type' => \Rebing\GraphQL\Support\PaginationType::class,
/*
* Config for GraphiQL (see (https://github.com/graphql/graphiql).
*/
'graphiql' => [
'prefix' => '/graphiql',
'controller' => \Rebing\GraphQL\GraphQLController::class.'#graphiql',
'middleware' => [],
'view' => 'graphql::graphiql',
'display' => env('ENABLE_GRAPHIQL', true),
],
/*
* Overrides the default field resolver
* See http://webonyx.github.io/graphql-php/data-fetching/#default-field-resolver
*
* Example:
*
* ```php
* 'defaultFieldResolver' => function ($root, $args, $context, $info) {
* },
* ```
* or
* ```php
* 'defaultFieldResolver' => [SomeKlass::class, 'someMethod'],
* ```
*/
'defaultFieldResolver' => null,
/*
* Any headers that will be added to the response returned by the default controller
*/
'headers' => [],
/*
* Any JSON encoding options when returning a response from the default controller
* See http://php.net/manual/function.json-encode.php for the full list of options
*/
'json_encoding_options' => 0,
];
You missed a few imports and return types, check the updated code:
app/GraphQL/Types/WineType.php
<?php
namespace App\GraphQL\Types;
use App\Wine;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Type as GraphQLType;
class WineType extends GraphQLType
{
protected $attributes = [
'name' => 'Wine',
'description' => 'Details about a wine',
'model' => Wine::class
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'Id of the wine',
],
'name' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The name of the wine',
],
'description' => [
'type' => Type::nonNull(Type::string()),
'description' => 'Short description of the wine',
],
'color' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The color of the wine',
],
'grape_variety' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The grape variety of the wine',
],
'country' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The country of origin of the wine',
]
];
}
}
app/GraphQL/Queries/WineQuery.php
<?php
namespace App\GraphQL\Queries;
use App\Wine;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class WineQuery extends Query
{
protected $attributes = [
'name' => 'wine',
];
public function type(): Type
{
return GraphQL::type('Wine');
}
public function args():array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
],
];
}
public function resolve($root, $args)
{
return Wine::findOrFail($args['id']);
}
}
app/GraphQL/Queries/WinesQuery.php
<?php
namespace App\GraphQL\Queries;
use App\Wine;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class WinesQuery extends Query
{
protected $attributes = [
'name' => 'wines',
];
public function type(): Type
{
return Type::listOf(GraphQL::type('Wine'));
}
public function resolve($root, $args)
{
return Wine::all();
}
}
Related
There's certainly something that I do wrong, but I can't use correctly the guard option of newEntity() method.
// Entity
class Bloc extends AbstractBloc
{
protected $_accessible = [
'*' => false // All fields are protected against mass assignment
];
}
'guard' => false doesn't allow to save my entity in this example :
// Controller
public function test()
{
$entity = $this->Blocs->newEntity([
'titre' => 'TEST ASSIGNEMENT',
'rubrique_id' => 282,
'description' => 'Content'
], ['guard' => false]); // guard is false but nothing changes
if ($this->Blocs->save($entity)) {
return $this->redirect([...]);
}
else {
die('save is false');
}
}
What am I doing wrong ?
There is no such option for Table::newEntity(), the guard option belongs to Entity::set(). The option for newEntity() is called accessibleFields.
$entity = $this->Blocs->newEntity(
[
'titre' => 'TEST ASSIGNEMENT',
'rubrique_id' => 282,
'description' => 'Content',
],
[
'accessibleFields' => [
// '*' => true
'titre' => true,
'rubrique_id' => true,
'description' => true,
],
]
);
https://book.cakephp.org/4/en/orm/saving-data.html#changing-accessible-fields
I'm using the laravel-graphql library from Folklore. I've followed all the instructions to the T, but when I test it out in the Altair Client for Windows, I get "Unknown Server Error, Code 0". My REST API works so it's either that I made an error in setting up the GraphQL layer or Folklore not updating their library has finally fallen behind to the Laravel version that I'm using. It's 5.7.
POSTTYPE.PHP
<?php
namespace App\GraphQL\Type;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;
/* Here we're giving the type a name and description */
class PostType extends GraphQLType {
protected $attributes = [
'name' => 'Post', //here we're defining the query by giving it a name of Post
'description' => 'One post'
];
public function fields(){
return [
'id' => [
'type' => Type::nonNull(Type::int()),
'description' => 'Primary key; incrementing id of post'
],
'header' => [
'type' => Type::string(),
'description' => 'The header of the post'
],
'body' => [
'type' => Type::string(),
'description' => 'The body of the blog post'
],
'created_at' => [
'type' => Type::string(),
'description' => 'When the post was created'
],
'updated_at' => [
'type' => Type::string(),
'description' => 'When the post was last updated'
],
'img' => [
'type' => Type::string(),
'description' => 'Where the main image of the blog post is stored'
],
];
}
// If you want to resolve the field yourself, you can declare a method
// with the following format resolve[FIELD_NAME]Field()
//protected function resolveEmailField($root, $args)
//{
// return strtolower($root->email);
//}
}
POSTQUERY.PHP
<?php
namespace App\GraphQL\Query;
use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Query;
use App\Post;
class PostsQuery extends Query {
//give the query a name of 'posts'
protected $attributes = [
'name' => 'posts'
];
//define the query type
public function type(){
return Type::listOf(GraphQL::type('Post'));
}
//define things to fetch and turn them into arguments
public function args(){
return [
'id' => ['name' => 'id', 'type' => Type::int()],
'header' => ['name' => 'header', 'type' => Type::string()],
'body' => ['name' => 'body', 'type' => Type::string()],
'img' => ['name' => 'img', 'type' => Type::string()],
];
}
//fetch all the posts
public function resolve($root, $args){
if (isset($args['id'])){
return Post::where('id', $args['id'])->get();
}
else if (isset($args['header'])){
return Post::where('header', $args['header'])->get();
}
else if (isset($args['body'])){
return Post::where('body', $args['body'])->get();
}
else if (isset($args['img'])){
return Post::where('img', $args['img'])->get();
}
else {
return Post::all();
}
}
}
CONFIG/GRAPHQL.PHP
<?php
return [
// The prefix for routes
'prefix' => 'graphql',
// The routes to make GraphQL request. Either a string that will apply
// to both query and mutation or an array containing the key 'query' and/or
// 'mutation' with the according Route
//
// Example:
//
// Same route for both query and mutation
//
// 'routes' => 'path/to/query/{graphql_schema?}',
//
// or define each routes
//
// 'routes' => [
// 'query' => 'query/{graphql_schema?}',
// 'mutation' => 'mutation/{graphql_schema?}'
// ]
//
// you can also disable routes by setting routes to null
//
// 'routes' => null,
//
'routes' => '{graphql_schema?}',
// The controller to use in GraphQL request. Either a string that will apply
// to both query and mutation or an array containing the key 'query' and/or
// 'mutation' with the according Controller and method
//
// Example:
//
// 'controllers' => [
// 'query' => '\Folklore\GraphQL\GraphQLController#query',
// 'mutation' => '\Folklore\GraphQL\GraphQLController#mutation'
// ]
//
'controllers' => '\Folklore\GraphQL\GraphQLController#query',
// Any middleware for the graphql route group
'middleware' => [],
// The name of the default schema used when no argument is provided
// to GraphQL::schema() or when the route is used without the graphql_schema
// parameter.
'schema' => 'default',
// The schemas for query and/or mutation. It expects an array to provide
// both the 'query' fields and the 'mutation' fields. You can also
// provide directly an object GraphQL\Schema
//
// Example:
//
// 'schemas' => [
// 'default' => new Schema($config)
// ]
//
// or
//
// 'schemas' => [
// 'default' => [
// 'query' => [
// 'users' => 'App\GraphQL\Query\UsersQuery'
// ],
// 'mutation' => [
//
// ]
// ]
// ]
//
'schemas' => [
'default' => [
'query' => [
'posts' => 'App\GraphQL\Query\PostsQuery'
],
'mutation' => [
'newPost' => 'App\GraphQL\Mutation\NewPostMutation',
//'updatePostStatus' => App\GraphQL\Mutation\UpdatePostStatusMutation::class,
]
]
],
// The types available in the application. You can then access it from the
// facade like this: GraphQL::type('user')
//
// Example:
//
// 'types' => [
// 'user' => 'App\GraphQL\Type\UserType'
// ]
//
// or whitout specifying a key (it will use the ->name property of your type)
//
'types' => [
'App\GraphQL\Type\PostType',
],
//
//'types' => [
// 'Post' => 'App\GraphQL\Type\PostType',
//],
// This callable will receive every Error object for each error GraphQL catches.
// The method should return an array representing the error.
//
// Typically:
// [
// 'message' => '',
// 'locations' => []
// ]
//
'error_formatter' => ['\Folklore\GraphQL\GraphQL', 'formatError']
];
-
-
UPDATE
So I installed the Rebing library and uninstalled the Folklore one, and got GraphiQL to work. I ran a query in it and got
"message": "Class App\\GraphQL\\Type\\UserType does not exist",
"exception": "ReflectionException",
"file": "C:\\wamp64\\www\\laravel-project\\vendor\\laravel\\framework\\src\\Illuminate\\Container\\Container.php",
"line": 779,
Something tells me I'm not supposed to be seeing paths like this. If this is wrong, how do I fix?
Ok so apparently the Rebing Laravel-GraphQL library (maybe the Folklore one too) handles cache poorly. I had to clear Laravel config cache every time I made a change. So if anyone gets into this problem, ensure you get GraphiQL working and clear your config for each change you make. Nothing is wrong with the double slashes. Apparently it's a UNIX thing. I've never seen that before (or never seen that in a bad error) so forgive me.
In your config/graphql instead of 'posts' => 'App\GraphQL\Query\PostsQuery'
use 'posts' => App\GraphQL\Query\PostsQuery::class.
It depends on version of PHP sometimes.
I extended the tt_content table with an relationship to the tags of the news extension. (So I can Tag all tt_content elements.)
The methods like findAll and findByUid always return an empty QueryResultInterface.
I created the extension with extensionbuilder.
I defined the Storage in the extensions right and ignore the storage on own methods.
ext_typoscript_setup.txt:
config.tx_extbase {
persistence {
classes {
EXAMP\ExampContentTagging\Domain\Model\ContentTagRelation {
mapping {
tableName = tt_content
recordType = Tx_ExampContentTagging_ContentTagRelation
}
}
}
}
}
ContentTagRelationRepository.php:
<?php
namespace EXAMP\ExampContentTagging\Domain\Repository;
/**
* The repository for ContentTagRelations
*/
class ContentTagRelationRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
/**
* #var array
*/
protected $defaultOrderings = array(
'sorting' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING
);
/**
* Find tt_content by a given pid
*
* #param array $includeIdList list of id s
* #param array $excludeIdList list of id s
* #return QueryInterface
*/
public function findByTagIdList(array $includeIdList, array $excludeIdList = [])
{
$query = $this->createQuery();
$query->getQuerySettings()->setRespectStoragePage(false);
$query->getQuerySettings()->setRespectSysLanguage(false);
return $query->execute();
}
TCA/Overrides/tt_content.php:
defined('TYPO3_MODE') || die();
if (!isset($GLOBALS['TCA']['tt_content']['ctrl']['type'])) {
if (file_exists($GLOBALS['TCA']['tt_content']['ctrl']['dynamicConfigFile'])) {
require_once($GLOBALS['TCA']['tt_content']['ctrl']['dynamicConfigFile']);
}
// no type field defined, so we define it here. This will only happen the first time the extension is installed!!
$GLOBALS['TCA']['tt_content']['ctrl']['type'] = 'tx_extbase_type';
$tempColumnstx_exampcontenttagging_tt_content = [];
$tempColumnstx_exampcontenttagging_tt_content[$GLOBALS['TCA']['tt_content']['ctrl']['type']] = [
'exclude' => true,
'label' => 'LLL:EXT:examp_content_tagging/Resources/Private/Language/locallang_db.xlf:tx_exampcontenttagging.tx_extbase_type',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'items' => [
['ContentTagRelation','Tx_exampContentTagging_ContentTagRelation']
],
'default' => 'Tx_exampContentTagging_ContentTagRelation',
'size' => 1,
'maxitems' => 1,
]
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $tempColumnstx_exampcontenttagging_tt_content);
}
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
'tt_content',
'tx_news_domain_model_tag'
);
$tmp_examp_content_tagging_columns = [
'tx_news_domain_model_tag' => [
'exclude' => true,
'label' => 'LLL:EXT:examp_content_tagging/Resources/Private/Language/locallang_db.xlf:tx_exampcontenttagging_domain_model_contenttagrelation.tx_news_domain_model_tag',
'config' => [
'type' => 'select',
'renderType' => 'selectMultipleSideBySide',
'foreign_table' => 'tx_news_domain_model_tag',
'MM' => 'tx_exampcontenttagging_contenttagrelation_tag_mm',
'size' => 10,
'autoSizeMax' => 30,
'maxitems' => 9999,
'multiple' => 0,
'wizards' => [
'_PADDING' => 1,
'_VERTICAL' => 1,
'edit' => [
'module' => [
'name' => 'wizard_edit',
],
'type' => 'popup',
'title' => 'Edit', // todo define label: LLL:EXT:.../Resources/Private/Language/locallang_tca.xlf:wizard.edit
'icon' => 'EXT:backend/Resources/Public/Images/FormFieldWizard/wizard_edit.gif',
'popup_onlyOpenIfSelected' => 1,
'JSopenParams' => 'height=350,width=580,status=0,menubar=0,scrollbars=1',
],
'add' => [
'module' => [
'name' => 'wizard_add',
],
'type' => 'script',
'title' => 'Create new', // todo define label: LLL:EXT:.../Resources/Private/Language/locallang_tca.xlf:wizard.add
'icon' => 'EXT:backend/Resources/Public/Images/FormFieldWizard/wizard_add.gif',
'params' => [
'table' => 'tx_news_domain_model_tag',
'pid' => '###CURRENT_PID###',
'setValue' => 'prepend'
],
],
],
],
],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content',$tmp_examp_content_tagging_columns);
/* inherit and extend the show items from the parent class */
if (isset($GLOBALS['TCA']['tt_content']['types']['1']['showitem'])) {
$GLOBALS['TCA']['tt_content']['types']['Tx_exampContentTagging_ContentTagRelation']['showitem'] = $GLOBALS['TCA']['tt_content']['types']['1']['showitem'];
} elseif(is_array($GLOBALS['TCA']['tt_content']['types'])) {
// use first entry in types array
$tt_content_type_definition = reset($GLOBALS['TCA']['tt_content']['types']);
$GLOBALS['TCA']['tt_content']['types']['Tx_exampContentTagging_ContentTagRelation']['showitem'] = $tt_content_type_definition['showitem'];
} else {
$GLOBALS['TCA']['tt_content']['types']['Tx_exampContentTagging_ContentTagRelation']['showitem'] = '';
}
$GLOBALS['TCA']['tt_content']['types']['Tx_exampContentTagging_ContentTagRelation']['showitem'] .= ',--div--;LLL:EXT:examp_content_tagging/Resources/Private/Language/locallang_db.xlf:tx_exampcontenttagging_domain_model_contenttagrelation,';
$GLOBALS['TCA']['tt_content']['types']['Tx_exampContentTagging_ContentTagRelation']['showitem'] .= 'tx_news_domain_model_tag';
$GLOBALS['TCA']['tt_content']['columns'][$GLOBALS['TCA']['tt_content']['ctrl']['type']]['config']['items'][] = ['LLL:EXT:examp_content_tagging/Resources/Private/Language/locallang_db.xlf:tt_content.tx_extbase_type.Tx_exampContentTagging_ContentTagRelation','Tx_exampContentTagging_ContentTagRelation'];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addLLrefForTCAdescr(
'',
'EXT:/Resources/Private/Language/locallang_csh_.xlf'
);
Mostly to this problem, the solution is to check if the right storage is set. But I did this and still have the problem. So I think tt_content is the problem. But the only thing I could find to this was:
"tt_content is special."
Which dosen't help. (Offtopic: This Typo3 Documentation makes me mad every time I've to look into it.)
In my module's module.config.php, I have something like this:
namespace Application;
return [
//...
// myroute1 will route to IndexController fooAction if the route is matching '/index/foo' but regardless of request method
'myroute1' => [
'type' => Zend\Router\Http\Literal::class,
'options' => [
'route' => '/index/foo',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'foo',
],
],
],
// myroute2 will route to IndexController fooAction if the route is request method is GET but regardless of requested route
'myroute2' => [
'type' => Zend\Router\Http\Method::class,
'options' => [
'verb' => 'get',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'foo',
],
],
],
//...
];
What I'm trying to achieve:
If route /index/foo is requested AND is requested by GET method, then it should be routed to IndexController fooAction
If route /index/foo is requested AND is requested by POST method, then it should be routed to IndexController barAction (notice it's barAction here not fooAction)
How to achieve that?
Try changing the literal to a Zend\Mvc\Router\Http\Part route, and then putting the HTTP routes in as CHILD routes!
See here https://docs.zendframework.com/zend-router/routing/#http-route-types
A note to myself and anyone else looking, as additional note to #delboy1978uk's answer.
The answer I was looking for is something like this:
GET /index/foo => IndexController fooAction
POST /index/foo => IndexController barAction
So the code in module.config.php file can be like this:
return [
//...
'myroute1' => [// The parent route will match the route "/index/foo"
'type' => Zend\Router\Http\Literal::class,
'options' => [
'route' => '/index/foo',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'foo',
],
],
'may_terminate' => false,
'child_routes' => [
'myroute1get' => [// This child route will match GET request
'type' => Method::class,
'options' => [
'verb' => 'get',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'foo'
],
],
],
'myroute1post' => [// This child route will match POST request
'type' => Method::class,
'options' => [
'verb' => 'post',
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => 'bar'
],
],
]
],
],
//...
];
I know this is an old topic but I wanted to share my answer for anyone who comes across this and is struggling with zend or Laminas (I use Laminas) and the routing of method based AND localized routes. Basically you should be able to just replace "Laminas" by "Zend" for the namespaces. The code base is very similar.
First of all: I was not able to use #evilReiko's solution because 'may_terminate' => false, always threw an exception for me. When I set it to true the child routes were ignored ... obviously :D
But the note helped me to understand some stuff going on. I decided to just implement a custom class which handles both: URL localization and method/action routing.
I created a new folder Classes and added a new file MethodSegment into modules/Application.
So the file path would be modules/Application/Classes/MethodSegment.php.
<?php
namespace Application\Classes;
use Laminas\Router\Exception;
use Laminas\Stdlib\ArrayUtils;
use Laminas\Stdlib\RequestInterface as Request;
use Laminas\Router\Http\RouteMatch;
use Traversable;
/**
* Method route.
*/
class MethodSegment extends \Laminas\Router\Http\Segment
{
/**
* associative array [method => action]
*
* #var array
*/
protected $methodActions;
/**
* Default values - accessing $defaults from parent class Segment
*
* #var array
*/
protected $defaults;
/**
* Create a new method route
*
* #param string $route
* #param array $constraints
* #param array $defaults
*/
public function __construct($route, array $constraints = [], array $defaults = [])
{
if(is_array($defaults['action']))
{
$this->methodActions = $defaults['action'];
$defaults['action'] = array_values($defaults['action'])[0];
}
parent::__construct($route, $constraints, $defaults);
}
/**
* factory(): defined by RouteInterface interface.
*
* #see \Laminas\Router\RouteInterface::factory()
*
* #param array|Traversable $options
* #return Method
* #throws Exception\InvalidArgumentException
*/
public static function factory($options = [])
{
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
} elseif (! is_array($options)) {
throw new Exception\InvalidArgumentException(sprintf(
'%s expects an array or Traversable set of options',
__METHOD__
));
}
if (! isset($options['defaults'])) {
$options['defaults'] = [];
}
return new static($options['route'] ?? null, $options['constraints'] ?? [], $options['defaults']);
}
/**
* match(): defined by RouteInterface interface.
*
* #see \Laminas\Router\RouteInterface::match()
*
* #return RouteMatch|null
*/
public function match(Request $request, $pathOffset = null, array $options = [])
{
if (! method_exists($request, 'getMethod')) {
return null;
}
$requestVerb = strtolower($request->getMethod());
$verb = array_keys($this->methodActions);
if (in_array($requestVerb, $verb)) {
$this->defaults['action'] = $this->methodActions[$requestVerb];
return parent::match($request, $pathOffset, $options);
}
return null;
}
}
Basically I copied the code from the Laminas Method class and enhanced it so I can pass an array of actions.
You can use the MethodSegment like so:
use App\Routing\MethodSegment;
return [
'router' => [
'routes' => [
'home' => [
'type' => MethodSegment::class,
'options' => [
'route' => /[:language/],
'constraints' => [...],
'defaults' => [
'controller' => Controller\IndexController::class,
'action' => [
'get' => 'index',
'post' => 'postIndex', // e.g. form
],
],
],
],
[...]
Hope this helps anyone, IMO the child route approach is very clunky.
I want to add a custom rule to PhpManager RBAC in Yii 2.0.
Here is the custom rule (#app/rbac/OwnerRule.php):
<?php
namespace app\rbac;
use yii\rbac\Rule;
/**
* Checks if userID matches user passed via params
*/
class OwnerRule extends Rule
{
public $name = 'isOwner';
public function execute($user, $item, $params)
{
$access = false;
if(isset($params['id'])){
// My custom logic used to set $access
}
return $access;
}
}
Here is the RBAC hierarchy file (#app/data/rbac.php)
<?php
use yii\rbac\Item;
return [
'manageThing0' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing1' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
'manageThing2' => ['type' => Item::TYPE_OPERATION, 'description' => '...', 'bizRule' => NULL, 'data' => NULL],
// AND THE ROLES
'guest' => [
'type' => Item::TYPE_ROLE,
'description' => 'Guest',
'bizRule' => NULL,
'data' => NULL
],
'user' => [
'type' => Item::TYPE_ROLE,
'description' => 'User',
'children' => [
'guest',
'manageThing0', // User can edit thing0
],
'bizRule' => 'return !Yii::$app->user->isGuest;',
'data' => NULL
],
'moderator' => [
'type' => Item::TYPE_ROLE,
'description' => 'Moderator',
'children' => [
'user', // Can manage all that user can
'manageThing1', // and also thing1
],
'bizRule' => NULL,
'data' => NULL
],
'admin' => [
'type' => Item::TYPE_ROLE,
'description' => 'Admin',
'children' => [
'moderator', // can do all the stuff that moderator can
'manageThing2', // and also manage thing2
],
'bizRule' => NULL,
'data' => NULL
],
];
How do I use my custom rule in the hierarchy file?
See these links hope you will find what you are looking for,
http://www.yiiframework.com/doc-2.0/guide-security-authorization.html
http://yii2-user.dmeroff.ru/docs/custom-access-control
RBAC for basic yii2 template