how to configure routes for child controllers in CakePHP 3.x? - cakephp

Reports has many ReportInstances
ReportInstances belongs to Reports
I would like to have the url /reports/:report-id/instances to point to the action index_by_report_id inside ReportInstancesController.php
How do I configure the routes.php accordingly?
UPDATE:
I tried nested resources as described here:
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
Here are my routes
$routes->resources('Reports', [
'map' => [
'standard' => [
'action' => 'standard',
'method' => 'GET',
]
]
]);
$routes->resources('Reports', function ($routes) {
$routes->resources('ReportInstances');
});
When I do a /reports/1/instances, it goes to ReportsController looking for action 1.
Please advise.

Do this in your routes.php
$routes->resources('Parents', function ($routes) {
$routes->resources('Children');
});
$routes->resources('Children');
In ChildrenController.php,
protected function _prepareConditions() {
$parentId = isset($this->request->params['parent_id']) ? $this->request->params['parent_id'] : null;
if ($parentId == null) {
return [];
}
return [
'Children.parent_id' => $parentId
];
}
public function index()
{
$conditions = $this->_prepareConditions();
$this->paginate = [
'contain' => ['Parents'],
'conditions' => $conditions
];
// ... and so on
You will be able to do the following:
/parents/1/children
/parents/1/children.json
/children
/children.json
Why this works?
http://book.cakephp.org/3.0/en/development/routing.html#creating-nested-resource-routes
tells us that basically to basically retrieve the parent id from the request params.
What it does not say explicitly is that, the routes will then reuse the basic 5 functions: index, add, view, delete, edit even when you nest them under a parent url.
Why do you still have a separate resources route for the Children?
This allows the /children and /children.json to work if you need them as well.
What about add?
I haven't tried that but I do not foresee any issues with using that as
/parents/1/children/add
/parents/1/children/add.json
/children/add
/children/add.json

Related

What do I do when I want to separate models with authentication plugins?

I'm currently implementing the login feature in CakePHP4.
So I wanted to divide it into a normal user model and an administrative user model.
I didn't know how to do this at all, and I spent all day looking for ways to implement it, but I couldn't do it.
I want to set up authentication in "routes.php", but I'm having trouble with "implementing AuthenticationServiceProviderInterface" in "Application.php". What should I do?
// routes.php
$routes->scope('/', function (RouteBuilder $builder) {
/......./
// ログイン
//$builder->connect('mgt-account/*', ['controller' => 'MgtAccount', 'action' => 'login', 'prefix' => 'Admin']);
$builder->connect('users/*', ['controller' => 'Users', 'action' => 'login', 'prefix' => 'Normal']);
/......./
});
$routes->prefix('Normal', function (RouteBuilder $routes) {
$loginUrl = Router::url('/normal/users/login');
$fields = [
'username' => 'mail',
'password' => 'password',
];
$service = new AuthenticationService([
'unauthenticatedRedirect' => $loginUrl,
'queryParam' => 'redirect',
]);
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form', [
'fields' => $fields,
'loginUrl' => $loginUrl,
]);
$service->loadIdentifier('Authentication.Password', compact('fields'));
$routes->registerMiddleware(
'auth',
new \Authentication\Middleware\AuthenticationMiddleware($service)
);
$routes->applyMiddleware('auth');
//$routes->connect('/:controller');
$routes->fallbacks(DashedRoute::class);
});
$routes->prefix('Admin', function (RouteBuilder $routes) {
$loginUrl = Router::url('/admin/mgt-account/login');
$fields = [
'username' => 'mail',
'password' => 'password',
];
$service = new AuthenticationService([
'unauthenticatedRedirect' => $loginUrl,
'queryParam' => 'redirect',
]);
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form', [
'fields' => $fields,
'loginUrl' => $loginUrl,
]);
$service->loadIdentifier('Authentication.Password', compact('fields'));
$routes->registerMiddleware(
'auth',
new \Authentication\Middleware\AuthenticationMiddleware($service)
);
$routes->applyMiddleware('auth');
//$routes->connect('/:controller');
$routes->fallbacks(DashedRoute::class);
});
<?php
// src/Application.php
declare(strict_types=1);
namespace App;
use Cake\Core\Configure;
use Cake\Core\Exception\MissingPluginException;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Psr\Http\Message\ServerRequestInterface;
/**
* Application setup class.
*
* This defines the bootstrapping logic and middleware layers you
* want to use in your application.
*/
class Application extends BaseApplication
// I really want to comment out and delete this bottom.
implements AuthenticationServiceProviderInterface
{
/**
* Load all the application configuration and bootstrap logic.
*
* #return void
*/
public function bootstrap(): void
{
// Call parent to load bootstrap from files.
parent::bootstrap();
if (PHP_SAPI === 'cli') {
$this->bootstrapCli();
}
/*
* Only try to load DebugKit in development mode
* Debug Kit should not be installed on a production system
*/
if (Configure::read('debug')) {
$this->addPlugin('DebugKit');
}
// Load more plugins here
}
/**
* Setup the middleware queue your application will use.
*
* #param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
* #return \Cake\Http\MiddlewareQueue The updated middleware queue.
*/
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
$middlewareQueue
// Catch any exceptions in the lower layers,
// and make an error page/response
->add(new ErrorHandlerMiddleware(Configure::read('Error')))
// Handle plugin/theme assets like CakePHP normally does.
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime'),
]))
// Add routing middleware.
// If you have a large number of routes connected, turning on routes
// caching in production could improve performance. For that when
// creating the middleware instance specify the cache config name by
// using it's second constructor argument:
// `new RoutingMiddleware($this, '_cake_routes_')`
->add(new RoutingMiddleware($this))
// I really want to comment out and delete this bottom.
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
/**
* Bootrapping for CLI application.
*
* That is when running commands.
*
* #return void
*/
protected function bootstrapCli(): void
{
try {
$this->addPlugin('Bake');
} catch (MissingPluginException $e) {
// Do not halt if the plugin is missing
}
$this->addPlugin('Migrations');
// Load more plugins here
}
// I really want to comment out and delete this bottom.
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
$authenticationService = new AuthenticationService([
'unauthenticatedRedirect' => '/normal/users/login',
'queryParam' => 'redirect',
]);
$authenticationService->loadIdentifier('Authentication.Password', [
'fields' => [
'username' => 'mail',
'password' => 'password',
]
]);
$authenticationService->loadAuthenticator('Authentication.Session');
$authenticationService->loadAuthenticator('Authentication.Form', [
'fields' => [
'username' => 'mail',
'password' => 'password',
],
'loginUrl' => '/normal/users/login',
]);
return $authenticationService;
}
}
As this is my first time using "stackoverflow", I'm not sure how to ask a good question. I'd appreciate it if you could help me out.
I would appreciate it if you could point it out to me.
thank you.
I believe the answer is to load the correct middleware authenticator based on the current request (admin or regular user). But I have not multiple authentications yet so might not be correct. In Application.php method getAuthenticationService(), where you call loadIdentifier(), specify one "resolver" or the other depending on the current request URL (or however you distinguish an admin url).
The CakePHP 4.x documentation has a section on multiple authentication schemes. I believe you can use two different tables. Configuring Multiple Authentication Setups
This forum item may have the answer you need (the answer below the question): Multiple Authentication Setups

How to use route elements instead of query string arguments for pagination in CakePHP 4.x?

My website's pagination urls worked like this, but stopped working when i upgraded to 4.x - for example: 1st page: mywebsite.com/new - 2nd page: mywebsite.com/new/page/2, etc. I did that with the following code:
//routes.php
$routes->connect('/new/page/:page',
['controller' => 'Articles', 'action' => 'latest'], [
'pass' => [
'page'
],
'page' => '\d+'
]);
$routes->connect('/new', ['controller' => 'Articles', 'action' => 'latest']);
then in my view/pagination-element I have the following:
//view
$this->Paginator->options(['url' => ['controller' => 'Articles', 'action' => 'latest']]);
The URLs mywebsite.com/new/page/2, 3, etc. are still working when accessing them directly, but links created through the paginator in the view are mywebsite.com/new?page=2, 3, etc. when they should be mywebsite.com/new/page/2, 3, etc.
can someone point me in the right direction?
I'd suggest that you consider possibly changing your URL schema to use the query argument style. Using route/URI elements just means more work to get the paginator features working properly. Also all the arguments (SEO, readability, etc) made for using "pretty URLs" for pagination, that were so popular back in the days, turned out to be nothing but hot air.
That being said, you'd have to use a custom/extended paginator helper to change how URLs are being generated, as by default the helper will explicitly set the page parameter (and all other pagination related parameters) as query string arguments. You have control over all generated URLs if you override \Cake\View\Helper\PaginatorHelper::generateUrlParams().
A quick and dirty example:
// src/View/Helper/PaginatorHelper.php
/*
Load in `AppView::initialize()` via:
$this->loadHelper('Paginator', [
'className' => \App\View\Helper\PaginatorHelper::class
]);
*/
declare(strict_types=1);
namespace App\View\Helper;
class PaginatorHelper extends \Cake\View\Helper\PaginatorHelper
{
public function generateUrlParams(array $options = [], ?string $model = null, array $url = []): array
{
$params = parent::generateUrlParams($options, $model, $url);
if (isset($params['?']['page'])) {
$params[0] = $params['?']['page'];
unset($params['?']['page']);
} else {
$params[0] = 1;
}
return $params;
}
}
That would move to the page parameter value in the URL array from the query string config to a regular URL parameter, ie turn
['controller' => 'Articles', 'action' => 'latest', '?' => ['page' => 1, /* ... */]]
into
['controller' => 'Articles', 'action' => 'latest', 1, '?' => [/* ... */]]
But again, I'd strongly suggest considering to switch to the query string URL schema.
See also
Cookbook > Views > Helpers > Configuring Helpers > Aliasing Helpers
Cookbook > Views > Helpers > Creating helpers

CakePHP 3.x Find records from current user only with paginate

This seems like a thing that is probably super easy in CakePHP, but I cannot find a clear example.
I would think this works in my controller:
public function index()
{
$this->paginate = [
'where' => ['user_id' => $this->Auth->user('id')],
'contain' => ['Users']
];
$slates = $this->paginate($this->Slates);
$this->set(compact('slates'));
$this->set('_serialize', ['slates']);
}
But I always get the full collection back. I am most definitely logged in, I have two unique users, I have created records with each of them.
Change where to conditions. Couldnt find a reference in the docs.
$this->paginate = [
'conditions' => ['user_id' => $this->Auth->user('id')],
'contain' => ['Users']
];

CakePHP 3 Add Action Routes to Index or Returns Null

I am trying to pass JSON into an Add() action of my controller but the response I get is either a response from the Index() action or "null" depending on entries in my routes.php (shown below).
I'm using Firefox plugin RESTClient to test/pass json to my actions:
Method: POST
URL: http://MyApp/notes.json (my understanding is this should call Add action based on cakephp docs )
JSON:
{
"post_id":"123",
"note":"hello world"
}
When I have the following in my routes.php, the response is "null"
Router::scope('/', function ($routes) {
$routes->extensions(['json']);
$routes->resources('Notes');
Removing $routes->resources('Notes'); from this scope returns a response from the index action and all items in the Notes model are returned in the response.
Furthermore, when I implement this through Crud.Add in an api prefix, I get valid results. That being said trying to understand what I have incorrect in my routes.php or add action that could be causing the null or routing to index action.
Add Action:
public function add()
{
$note = $this->Notes->newEntity();
if ($this->request->is('post')) {
$note = $this->Notes->patchEntity($note, $this->request->data);
if ($this->Notes->save($note)) {
$message = 'Saved';
} else {
$message = 'Error';
}
}
$this->set('_serialize', ['note', 'message']);
}
Routes.php:
<?php
use Cake\Core\Plugin;
use Cake\Routing\Router;
Router::defaultRouteClass('DashedRoute');
Router::extensions(['json'], ['xml']);
Router::prefix('api',function($routes) {
$routes->extensions(['json','xml']);
$routes->resources('Notes');
});
Router::scope('/', function ($routes) {
$routes->extensions(['json']);
$routes->resources('Notes');
$routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);
$routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);
$routes->fallbacks('DashedRoute');
});
Plugin::routes();
Thanks in advance.
You didn't set any variables for the view, other than serialization options, so you get null because there's nothing to serialize, ie you are missing something like
$this->set(compact('note', 'message'));
in your action.
And without the resource routes a request to /notes maps to the index action of the Notes controller, because that's how fallback routes are connected, no action = index action.
See also
Cookbook > Views > JSON And XML Views > Using Data Views with the Serialize Key
Cookbook > Routing > Fallbacks Method

cakephp 3.0 isAuthorized() not being called

I've followed the tutorial and all the CakePHP Authorization guide and I can't get my isAuthorized() method to be called. My understanding (correct me if I am wrong, which is incredibly likely) is by delegating authorize to the specific controllers by doing 'authorize'->['Controller'] in AppController.php, when a method in UsersController is called, in this case 'add', UsersController would run the isAuthorized() method I defined. I was testing to see if this method ran at all outputting a flash->error message right when isAuthorized() is called but nothing happens. If I explicitly call isAuthorized($hardcodeduser) in my beforeFilter()method it will work but only if I hard code a user.
The way the method is supposed to work is: If a registered user requests to add/create a new user, the system checks to see if the user has admin/staff level permissions (which is just a 0 or 1 value in the database) and if the user does not have permission then it redirects to the home screen with an error message that says "You are not authorized to access that function".
Any help or suggestions or other links to follow would be much appreciated!
class AppController extends Controller {
public $components = ['Flash', 'Auth', 'Session'];
public function initialize() {
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'authorize' => ['Controller'],
'loginRedirect' => [
'controller' => 'Articles',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
}
public function beforeFilter(Event $event) {
$this->Auth->authorize = 'Controller';
}
public function isAuthorized($user) {
if(isset($user['is_staff']))
return true;
return false;
}
}
class UsersController extends AppController {
public function beforeFilter(Event $event) {
parent::beforeFilter($event);
$this->Auth->allow(['logout']);
}
public function isAuthorized($user) {
$this->Flash->error(__('Test Message PLEASE WORK'));
if($this->request->action === 'add') {
$isStaff = $user['is_staff'];
if($isStaff == 0) {
$this->redirect($this->Auth->redirectUrl());
$this->Flash->error(__('Not authorized to access this function'));
return false;
}
}
return parent ::isAuthorized($user);
}
}
Generally your assumption is correct, Controller::isAuthorized() is going to be invoked automatically when using the controller authorization handler.
The problem with your code is that in your UsersController::beforeFilter() method you are explicitly allowing the add method to be accessed by everyone (it won't even require authentication):
$this->Auth->allow(['logout', 'add']);
You have to understand that once a method is allowed, there will be no further checks made by the auth component, see AuthComponent::startup().
Also note that you don't need to redirect and set a flash message manually, the component will do that for you, you just need to configure it appropriately using the authError and unauthorizedRedirect options, see Cookbook > Components > Authentication > Configuration options
As we following the Cake blog tutorial,
they made a little mistake, that function "isAuthorized" never be called.
And I did take a time to research it.
Solution is
Adding this line when load component "Auth":
'authorize' => array('Controller'),
so the code should looks something like this:
$this->loadComponent('Auth', [
'loginRedirect' => [
'controller' => 'Articles',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
],
'authorize' => array('Controller'),
]);
Hope it help some one saving time :)
From cakephp 3.x documentation: you can configure authorization handlers in your controller’s beforeFilter() or initialize() methods using an array:
// Basic setup
$this->Auth->config('authorize', ['Controller']);
// Pass settings in
$this->Auth->config('authorize', [
'Actions' => ['actionPath' => 'controllers/'],
'Controller'
]);

Resources