How to set an attribute to route definition in Slim4 and use it in a middleware - slim

I need to set a custom attribute in the route definition and use it a route middleware. For example, I need to manage the refer page to redirect the user after the login.
This is my routes definition:
return function (App $app) {
$app->get('/', Home::class. ':home')->setName('home');
$app->get('/login', UserAction::class. ':getLogin')->setName('login')->setAttribute('norefer',true);
$app->post('/login', UserAction::class. ':postLogin');
};
The ->setAttribute('norefer',true); is what I'm looking for and seems it doesn't exist.
I need this attribute using ->getAttribute("norefer") in a middleware so I can store the last referable page visited by the user:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
if (!empty($route) && !$routeContext->getRoute()->getAttribute("norefer")) {
$referName = $routeContext->getRoute()->getName();
$referArgs = $routeContext->getRoute()->getArguments();
$this->session->set("referName", $referName);
$this->session->set("referArgs", $referArgs);
}
return $handler->handle($request);
}
So, in the session I can store the last referable page and use it after the login process to redirect the user to his page.

You could add a NoRefererMiddleware to routes you want to exclude from the redirection logic. NoRefererMiddleware just sets a noreferer attribute to the request object if its called.
<?php
use App\Middleware\NoRefererMiddleware;
use Slim\App;
return function (App $app) {
$app->get('/', Home::class. ':home')->setName('home');
$app->get('/login', UserAction::class. ':getLogin')->setName('login')->add(NoRefererMiddleware::class);
$app->post('/login', UserAction::class. ':postLogin');
};
File: src/Middleware/NoRefererMiddleware.php
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class NoRefererMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$request = $request->withAttribute('noreferer', true);
return $handler->handle($request);
}
}
Usage
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$noReferer = $request->getAttribute('noreferer');
if ($noReferer !== true) {
$routeContext = RouteContext::fromRequest($request);
$route = $routeContext->getRoute();
if ($route !== null) {
$referName = $routeContext->getRoute()->getName();
$referArgs = $routeContext->getRoute()->getArguments();
$this->session->set('referName', $referName);
$this->session->set('referArgs', $referArgs);
}
}
return $handler->handle($request);
}

Related

Redirect inside a service class?

I've created my own service class and I have a function inside it, handleRedirect() that's supposed to perform some minimal logical check before choosing to which route to redirect.
class LoginService
{
private $CartTable;
private $SessionCustomer;
private $Customer;
public function __construct(Container $SessionCustomer, CartTable $CartTable, Customer $Customer)
{
$this->SessionCustomer = $SessionCustomer;
$this->CartTable = $CartTable;
$this->Customer = $Customer;
$this->prepareSession();
$this->setCartOwner();
$this->handleRedirect();
}
public function prepareSession()
{
// Store user's first name
$this->SessionCustomer->offsetSet('first_name', $this->Customer->first_name);
// Store user id
$this->SessionCustomer->offsetSet('customer_id', $this->Customer->customer_id);
}
public function handleRedirect()
{
// If redirected to log in, or if previous page visited before logging in is cart page:
// Redirect to shipping_info
// Else
// Redirect to /
}
public function setCartOwner()
{
// GET USER ID FROM SESSION
$customer_id = $this->SessionCustomer->offsetGet('customer_id');
// GET CART ID FROM SESSION
$cart_id = $this->SessionCustomer->offsetGet('cart_id');
// UPDATE
$this->CartTable->updateCartCustomerId($customer_id, $cart_id);
}
}
This service is invoked in the controller after a successful login or registration. I'm not sure what's the best way to access redirect()->toRoute(); from here (or if I should do it here).
Also if you have other comments on how my code is structured please feel free to leave them.
Using plugins within your services is a bad idea as they require a controller to be set. When a service is created and you inject a plugin it has no idea of the controller instance so it will result in an error exception. If you want to redirect the user you might just edit the response object as the redirect plugin does.
Notice that I stripped the code to keep the example clear and simple.
class LoginServiceFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new LoginService($container->get('Application')->getMvcEvent());
}
}
class LoginService
{
/**
* #var \Zend\Mvc\MvcEvent
*/
private $event;
/**
* RedirectService constructor.
* #param \Zend\Mvc\MvcEvent $event
*/
public function __construct(\Zend\Mvc\MvcEvent $event)
{
$this->event = $event;
}
/**
* #return Response|\Zend\Stdlib\ResponseInterface
*/
public function handleRedirect()
{
// conditions check
if (true) {
$url = $this->event->getRouter()->assemble([], ['name' => 'home']);
} else {
$url = $this->event->getRouter()->assemble([], ['name' => 'cart/shipping-info']);
}
/** #var \Zend\Http\Response $response */
$response = $this->event->getResponse();
$response->getHeaders()->addHeaderLine('Location', $url);
$response->setStatusCode(302);
return $response;
}
}
Now from within your controller you can do the following:
return $loginService->handleRedirect();

Symfony 3 rest API query string

I'm working on a project written in Symfony 3 and I have to make a REST API Controller.
I have classic rout for example:
/users : (GET)get all users
/users/{id} : (GET) get a single user
/users : (POST) create a user
and so on..
But I would like to know how to implement a route to search with multiple parameter a user like this URL:
/users?name=John&surname=Doe&age=20&city=London
How can I create this route with query string and search inside it if a value isn't set?
This is a piece of my controller
namespace AppBundle\Controller;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Controller\FOSRestController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use AppBundle\Entity\User;
use AppBundle\Form\UserType;
class UserController extends FOSRestController
{
/**
* #Rest\Get("/users")
*/
public function getUsersAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$users = $em->getRepository(User::class)->findAll();
if (!$users) {
throw new HttpException(400, "Invalid data");
}
return $users;
}
/**
* #Rest\Get("/users/{userId}")
*/
public function getUsersByIdAction($userId, Request $request)
{
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository(User::class)->find($userId);
if (!$userId) {
throw new HttpException(400, "Invalid id");
}
return $user;
}
/**
* #Rest\Post("/users")
*/
public function postUsersAction(Request $request)
{
$user = new User();
$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $user;
//return new JsonResponse( [$data, $status, $headers, $json])
}
throw new HttpException(400, "Invalid data");
}
}
As usual you have to choices here:
The quick, easy, dirty one:
Using a query parameter you can add individual parameters to your controller:
Example:
/**
* #Rest\Get("/users/{userId}")
* #QueryParam(name="foo")
*/
public function getUsersByIdAction($userId, Request $request, $foo)
{
Documentation
The slower, safer, cleaner one:
Build a custom form type to process whatever parameters might have been included in your request/query and map them to a proper object which you can then use to extract the parsed values from and pass along to your query builder/ repository / manager.
Documentation

how to config container with request variable

In the first place I had to configure parameters using the class "ParametersCompilerPass" to get data from database.Here si my class :
class ParametersCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$em = $container->get('doctrine.orm.default_entity_manager');
$boutique = $em->getRepository('AcmeBundle:Boutique')->findOneByNom($container->getParameter('boutique.config'));
if(null !== $boutique){
$container->setParameter('url_site', $boutique->getUrl());
$container->setParameter('idboutique', $boutique->getId());
}else{
$container->setParameter('url_site', null);
$container->setParameter('idboutique', 0);
}
}
}
and when i set a parameter from request, it dont work, i tried in adding this code :
$request = $container->get('request_stack')->getCurrentRequest();
if($request->getMethod() == 'POST'){
if (null !== $choixbout = $request->get('choixbout')){
// $this->container->setParameter('idboutique',$choixbout);
}
}
the service request_stack return null.
I do not know how to configure a parameter from a POST variable.
Hope you can help me.
thanks
Is it solid requirement to have the parameter set?
It could be handy to create a service which has a request dependency that can act as a boutique parameter holder.
For example
# app/config/services.yml
app.boutique:
class: AppBundle\Boutique\Boutique
arguments: ['#request_stack']
app.boutique_info_dependant1:
class: AppBundle\Boutique\BoutiqueDependant1
arguments: ['#app.boutique']
app.boutique_info_dependant2:
class: AppBundle\Boutique\BoutiqueDependant2
arguments: ['#app.boutique']
This would be a parameter handler.
# AppBundle/Boutique/Boutique.php
class Boutique
{
/** #var RequestStack */
private $requestStack;
/**
* BoutiqueListener constructor.
* #param ContainerInterface $container
*/
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getBoutique()
{
$request = $this->requestStack->getCurrentRequest();
/// here you can add an extra check if the request is master etc.
if ($request->getMethod() == Request::METHOD_POST) {
if (null !== $choixbout = $request->get('choixbout')) {
return $choixbout;
}
}
return null;
}
}
Then using the handler
class BoutiqueDependant1
{
public function __construct(Boutique $boutique)
{
$this->myBoutique = $boutique->getBoutique();
}
}
This does not look like the best solution but could work...
Other option would be to rethink the application architecture to handle boutique information somehow differently.

Zend ACL Dynamic Assertion

I want to restrict my users to edit/delete only the comments which they added. I found an example on youtube by a guy named intergral30 and followed his instruction. And now my admin account has the possibility to edit/delete everything, but my user has no access to his own comment.
Here's the code:
Resource
class Application_Model_CommentResource implements Zend_Acl_Resource_Interface{
public $ownerId = null;
public $resourceId = 'comment';
public function getResourceId() {
return $this->resourceId;
}
}
Role
class Application_Model_UserRole implements Zend_Acl_Role_Interface{
public $role = 'guest';
public $id = null;
public function __construct(){
$auth = Zend_Auth::getInstance();
$identity = $auth->getStorage()->read();
$this->id = $identity->id;
$this->role = $identity->role;
}
public function getRoleId(){
return $this->role;
}
}
Assertion
class Application_Model_CommentAssertion implements Zend_Acl_Assert_Interface
{
public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $user=null,
Zend_Acl_Resource_Interface $comment=null, $privilege=null){
// if role is admin, he can always edit a comment
if ($user->getRoleId() == 'admin') {
return true;
}
if ($user->id != null && $comment->ownerId == $user->id){
return true;
} else {
return false;
}
}
}
In my ACL I have a function named setDynemicPermissions, which is called in an access check plugin's preDispatch method.
public function setDynamicPermissions() {
$this->addResource('comment');
$this->addResource('post');
$this->allow('user', 'comment', 'modify', new Application_Model_CommentAssertion());
$this->allow('admin', 'post', 'modify', new Application_Model_PostAssertion());
}
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
$this->_acl->setDynamicPermissions();
}
And I'm calling the ACL-s isAllowed method from my comment model where I return a list of comment objects.
public function getComments($id){
//loading comments from the DB
$userRole = new Application_Model_UserRole();
$commentResource = new Application_Model_CommentResource();
$comments = array();
foreach ($res as $comment) {
$commentResource->ownerId = $comment[userId];
$commentObj = new Application_Model_Comment();
$commentObj->setId($comment[id]);
//setting the data
$commentObj->setLink('');
if (Zend_Registry::get('acl')->isAllowed($userRole->getRoleId(), $commentResource->getResourceId(), 'modify')) {
$commentObj->setLink('Edit'.'Delete');
}
$comments[$comment[id]] = $commentObj;
}
}
Can anyone tell me what have I done wrong?
Or what should I use if I want to give my admins the right to start a post and other users the right to comment on them. Each user should have the chance to edit or delete his own comment and an admin should have all rights.
You seem to be using the dynamic assertions in a wrong manner, as you are still passing the roleId to isAllowed().
What these dynamic assertions really do, is take a complete object and work with it. Zend will determine which rule has to be used by calling getResourceId() and getRoleId() on your objects.
So all you have to do is pass your objects instead of the strings to isAllowed():
public function getComments($id){
//loading comments from the DB
$userRole = new Application_Model_UserRole();
$commentResource = new Application_Model_CommentResource();
$comments = array();
foreach ($res as $comment) {
$commentResource->ownerId = $comment[userId];
$commentObj = new Application_Model_Comment();
$commentObj->setId($comment[id]);
//setting the data
$commentObj->setLink('');
// This line includes the changes
if (Zend_Registry::get('acl')->isAllowed($userRole, $commentResource, 'modify')) {
$commentObj->setLink('Edit'.'Delete');
}
$comments[$comment[id]] = $commentObj;
}
}
But in can be done better
You would not have to implement a total new Application_Model_CommentResource, but instead you can use your actual Application_Model_Comment like this:
// we are using your normal Comment class here
class Application_Model_Comment implements Zend_Acl_Resource_Interface {
public $resourceId = 'comment';
public function getResourceId() {
return $this->resourceId;
}
// all other methods you have implemented
// I think there is something like this among them
public function getOwnerId() {
return $this->ownerId;
}
}
Assertion would then use this object and retrieve the owner to compare it with the actually logged in person:
class Application_Model_CommentAssertion implements Zend_Acl_Assert_Interface {
public function assert(Zend_Acl $acl, Zend_Acl_Role_Interface $user=null,
Zend_Acl_Resource_Interface $comment=null, $privilege=null){
// if role is admin, he can always edit a comment
if ($user->getRoleId() == 'admin') {
return true;
}
// using the method now instead of ->ownerId, but this totally depends
// on how one can get the owner in Application_Model_Comment
if ($user->id != null && $comment->getOwnerId() == $user->id){
return true;
} else {
return false;
}
}
And the usage is like this:
public function getComments($id) {
//loading comments from the DB
$userRole = new Application_Model_UserRole();
$comments = array();
foreach ($res as $comment) {
$commentObj = new Application_Model_Comment();
$commentObj->setId($comment[id]);
//setting the data
$commentObj->setLink('');
// no $commentResource anymore, just pure $comment
if (Zend_Registry::get('acl')->isAllowed($userRole, $comment, 'modify')) {
$commentObj->setLink('Edit'.'Delete');
}
$comments[$comment[id]] = $commentObj;
}
}

ZF using placeholders inside plugins

I'm trying to implement a plugin that handles responses to the user on success or failure in a persistance transaction. When the response is false I use a _forward to the action that performed the form's submit and get my placeholder message shown but when the response is true I use a _redirect to the browse with the new record shown.
My problem is that when I use _redirect the browser doesn't show the placeholder message. I'll show the code here:
/**
* Plugin
*/
class Application_Plugin_PostMessage extends Zend_Controller_Plugin_Abstract
{
public function postDispatch(Zend_Controller_Request_Abstract $request)
{
$message = $request->getParam('message');
$error = $request->getParam('error');
if (null !== $message || null !== $error) {
$layout = Zend_Layout::getMvcInstance();
$view = $layout->getView();
$placeHolder = $view->placeholder('message');
$placeHolder->setPostfix('</div>');
if (null !== $error) {
$placeHolder->setPrefix('<div class="errorMessage">')
->append($error);
}
elseif (null !== $message) {
$placeHolder->setPrefix('<div class="infoMessage">')
->append($message);
}
}
}
}
/**
* Controller
*/
class My_FooController extends Zend_Controller_Action
{
public function init()
{
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Application_Plugin_PostMessage());
}
...
public function browseAction()
{
...
// No message is shown here on redirect
...
}
public function newAction()
{
...
// This code shows the placeholder on _forward call
...
}
public function insertAction()
{
if(true) {
return $this->_redirect('/my/foo/browse?message='
. urlencode("success message"));
}
else {
return $this->_forward('new', null, null, array(
'error' => 'error messsage'
));
}
}
}
I can't use _forward on success because I don't want the use of [F5] key repeats the insert action
Thanks in advance
This is what Flash Messenger is for:
http://framework.zend.com/manual/en/zend.controller.actionhelpers.html#zend.controller.actionhelper.flashmessenger.basicusage
It stores messages in your session removing the need for passing messages as you are.