I've got a notification system where a route name is stored in the DB along with some a list of parameters used to build a URL. When a user clicks on a notification, these values are recalled and are passed into the router to generate a URL for the user to be redirected to.
A notification has the following structure (simplified structure, there is some inheritance at play here)
class Notification {
/** #var int */
protected $id;
/** #var Uid */
protected $uid;
/** #var string */
protected $title;
/** #var string */
protected $routeName;
/** #var array|null */
protected $routeParameters;
/** #var DateTime */
protected $date;
/** #var DateTime|null */
protected $read;
}
However instead of blindly passing these parameters into the clickable object on screen, they pass through the a redirectAction() defined in the NotificationController.
{{ notification.title }}
This so that I can do a couple things.
Mark the notification as read
Make sure that the user who clicked the notification is the user who the notification was delivered to (otherwise throw an AccessDeniedException)
Make sure that old links are handled nicely <-- the issue
So far, there is a section in my controller that reads the following:
/**
* #Route("/notification/action", name="notification_action")
* #param Request $request
* #param RouterInterface $router
* #return RedirectResponse|Response
*/
public function redirectAction(Request $request, RouterInterface $router) {
// * $notification gets recalled from the DB from the 'uid' in the request params
// * Validation is applied to the notification (check if exists, check if for the correct user)
// * Notification is marked as read with the current timestamp
try {
$url = $router->generate($notification->getRoute(), $notification->getParameters() ?? []);
} catch(RouteNotFoundException | MissingMandatoryParametersException | InvalidParameterException $e) {
return $this->render('notification/broken_action.html.twig');
}
return $this->redirect($url);
}
This is in order to attempt to generate a route and nicely handle a route that either
a) doesn't exist
b) no longer works with older parameters.
This works great if I were to change the definition of a route in the future, but it doesn't handle checking if the user has permission to access the route. This includes:
Checking if the user has the appropriate role to access the route (defined in security.yaml)
Checking if they user is denied access by either the #Security annotation or a voter
Validating parameters interpreted by the the #ParamConverter (such as when an entity may not exist anymore causing a 404)
Handling a thrown exception such as AccessDeniedException or NotFoundHttpException from logic within the action itself
I'm having trouble coming up with a way that I can validate these things before redirecting the user to the $url.
So far all I've come up with is performing a dummy request and seeing if that works before then redirecting the user, but that is yuck for obvious reasons (double request will have extended load times, potentially triggering an action in the background, etc.)
Is there a way that I can check for these things in an efficient way before sending the user to the generated URL? I'm happy to let logic within the action itself slide through validation, as most of the time I tend to avoid this in favour for voters or use of the #Security and #ParamConverter annotations instead. It's just a nice to have for edge cases.
...or I'm open to other ideas on better ways to handle clicking on a notification!
Related
Context
I have an entity User and an entity Cart in my project. There is a ManyToOne relation between both entities :
/* /src/Entity/User.php */
/**
* #ORM\OneToMany(targetEntity="App\Entity\Cart", mappedBy="user", orphanRemoval=true)
*/
private $carts;
/* /src/Entity/Cart.php */
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="carts")
* #ORM\JoinColumn(nullable=false)
*/
private $user;
I also have a method in my CartRepository that returns the current cart of the user.
/*src/Repository/CartRepository.php */
/**
* #param User $user
* #return mixed
* #throws NonUniqueResultException If the user somehow has several non-validated
* carts bidden to their account.
*/
public function findCurrentUserCart(User $user)
{
/* ... */
}
My Problem
What I would like to do is to be able to access easily the current count of cart items in the current user cart, so I can display it in my navbar. To do so, I need to be able to access the user's current cart easily.
I thought about getting it through the controller, but it seems quite annoying because, as the information is requested on all the pages of the site (because it is in the navbar) then I would need to retrieve the count from every single action of the controllers.
I then thought about having a simple function in my User entity like getCartTotalCount() but this would imply using the CartRepository directly from my User entity which, IMHO, feels wrong (I mean, my entity is a plain object and I don't feel like getting a repository is a good way to go, is it ?)
So my question is : Is there a way to globally get the current cart object, so I can have it in all my views, without needing to passing it in any single render() method in my controllers ? Or maybe is there another way to go ?
Presuming you’re using twig to render your templates, you have to create an extension.
Declaring a twig extension as a service you can inject the entity manager or the repository directly in the service constructor.
Here’s the documentation on symfony docs on how to create a twig extension: https://symfony.com/doc/current/templating/twig_extension.html
In this case you need to create a custom twig function.
Even if the Twig extension looks like a feasible solution, I'd do it differently: using an embedded controller, you have more control. Place the cart template snippet in a distinct file, create a controller that renders this tiny piece, and call it using
{{ render(controller(
'App\\Controller\\CartController::cartNavbar'
)) }}
If the template to display this information grows (for example, as you want to use an icon for an empty cart, and another one if something is placed in the cart), you will see that this helps to seperate concerns better than using an extension
Before I dive into reinventing the wheel, I'd first like to check if ZF2 supports, either out-of-the-box or with a 3rd party library, this particular use case where admins log in as other user, or assume their identity.
If not, as I'm not familiar with ZF2 internal design, how would I go into implementing this, with the only constraint being that the system is already built, so I can't change components (controllers, auth services, etc) into supporting it.
My first thought would be to make a mechanism to switch the logged user information stored in the session storage, with the one whose identity I want to assume. Then, write to the session, under a different namespace, the original user information (admin) so that it can be reverted.
Going by this approach, I am expecting components like Zend\Authentication\AuthenticationService return the user whose identity I'm assuming. So, in every call I make to $this->identity()->getId() (identity being a controller plugin for AuthenticationService, that returns the User) in other controllers, the business logic will work normally.
Having said this, the questions would be:
Is there a solution already for this?
Is my approach correct in assuming that by overwriting the session storage I can assume other user ID and expect ZF2 components to work accordingly, or is there any considerations regarding ZF2 internal design/infrastructure I haven't taken in consideration that I should?
Maybe there's a better way to do this?
I think you would need to create your own AuthenticationAdaptor.
class AdminUserLoginAsUser implements \Zend\Authentication\Adapter\AdapterInterface
{
/**
* #var User
*/
private $userToLoginAs;
/**
* #var AdminUser
*/
private $adminUser;
public function __construct(User $userToLoginAs, AdminUser $adminUser)
{
$this->userToLoginAs = $userToLoginAs;
$this->adminUser = $adminUser;
}
/**
* Performs an authentication attempt
*
* #return \Zend\Authentication\Result
* #throws \Zend\Authentication\Adapter\Exception\ExceptionInterface If authentication cannot be performed
*/
public function authenticate()
{
return new \Zend\Authentication\Result(
Result::SUCCESS, $this->user, [
'You have assumed control of user.',
]
);
}
}
The above class will allow you to login as another user when used with Zend's AuthenticationService class.
You will need some way of using Zend's AuthenticationService class and I would recommend using an AuthManager that wraps around the AuthenticationService.
/**
* The AuthManager service is responsible for user's login/logout and simple access
* filtering. The access filtering feature checks whether the current visitor
* is allowed to see the given page or not.
*/
class AuthManager
{
/**
* Authentication service.
* #var \Zend\Authentication\AuthenticationService
*/
private $authService;
/**
* Session manager.
* #var Zend\Session\SessionManager
*/
private $sessionManager;
/**
* Contents of the 'access_filter' config key.
* #var array
*/
private $config;
/**
* Constructs the service.
*/
public function __construct($authService, $sessionManager, $config)
{
$this->authService = $authService;
$this->sessionManager = $sessionManager;
$this->config = $config;
}
/**
* Performs a login attempt. If $rememberMe argument is true, it forces the session
* to last for one month (otherwise the session expires on one hour).
*/
public function login($email, $password, $rememberMe)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity()!=null) {
throw new \Exception('Already logged in');
}
// Authenticate with login/password.
$authAdapter = $this->authService->getAdapter();
$authAdapter->setEmail($email);
$authAdapter->setPassword($password);
$result = $this->authService->authenticate();
// If user wants to "remember him", we will make session to expire in
// one month. By default session expires in 1 hour (as specified in our
// config/global.php file).
if ($result->getCode()==Result::SUCCESS && $rememberMe) {
// Session cookie will expire in 1 month (30 days).
$this->sessionManager->rememberMe(60*60*24*30);
}
return $result;
}
public function loginAsUser($user)
{
// Check if user has already logged in. If so, do not allow to log in
// twice.
if ($this->authService->getIdentity() !== null) {
throw new \Exception('Not logged in.');
}
// First need to logout of current user
$this->authService->clearIdentity();
$authAdapter = $this->authService->setAdapter(new AdminUserLoginAsUser($user, $this->authService->getIdentity()));
return $this->authService->authenticate();
}
/**
* Performs user logout.
*/
public function logout()
{
// Allow to log out only when user is logged in.
if ($this->authService->getIdentity()==null) {
throw new \Exception('The user is not logged in');
}
// Remove identity from session.
$this->authService->clearIdentity();
}
}
To see how to plug it all together I would recommend looking at the following resources:
https://olegkrivtsov.github.io/using-zend-framework-3-book/html/en/User_Management__Authentication_and_Access_Filtering.html
https://github.com/olegkrivtsov/using-zf3-book-samples/tree/master/userdemo/module/User
The resources are for zf3 but I think the authenticating of users and managing authentication is very similar to zf2.
I am new to laravel and creating my first controller in this , i have created a file in directory app/controllers/ContactController.php and the code is
class ContactController extends BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
//
echo "hieeeeeeeeeeeeeeeee";
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store()
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id)
{
//
}
}
but if i hit url http://localhost:8000/contact it is showing me the error We need a map. am i missing something???please help me.
update
i also tried to add Route::get('contact', "contact#index"); in my routes.php.
i setup an user log in and register module through git-hub and it is working perfectly if i hit url http://localhost:8000/user/login
Update
my laravel version is laravel 4
output in console is 39023 Invalid request (Unexpected EOF)
I fully agree with the things #Raviraj Chauhan already pointed out. in addition, i want to add that your file seems to have a typo which can cause this kind of issue:
Rename your controller-class to ContactController and the containing file to ContactController.php (not contactCtroller.php).
Then add a route to your routes.php-file
Route::get("contact", "ContactController#index");
Generally pay attentions to common conventions and codings-practices, since laravel heavyly depends on concepts such as Convention over Configuration.
As Lukas also pointed out it might be smart to think about switching to Laravel 5 if you are just getting startet.
Anyway, let me finish by recommending laracasts, which is how i learned using laravel. It will only take you a couple of hours to dive deeply into the laravel-universe without to much knowledge needed in advance:
Laravel 4 From Scratch
Laravel 5 Funcamentals (no need to go through L4 from Scratch before)
Yes, There is a problem with your route.
If you want to point to a single method from a controller, then you have to specify a fullControllerName#methodName convention.
Fix your route in routes.php as:
Route::get('contact', "contactController#index");
Also please follow good class naming convention while using OOP.
Controller class name shold start with capital letter.
It should contain Controller at end.
So do fix by renaming your controller class name as well as controller file name, and do:
Route::get('contact', "ContactController#index");
And by easy way, do it by command line by running:
php artisan make:controller ContactController
I was developing codeigniter app for last 2 years and since i started my mvc pattern styling from codeigniter itself, now i'm not a heavy command line user so at first i did had some learning curve with codeigniter but i crossed it and started developing apps with code igniter because we don't need to configure a lot and everything was inside one zip file but now as unfortunately codeigniter is dead and i'm just one person in my team i have to rely on other third party tools which are trusted by others so i decided to switch to laravel, now at starting it was way way tough to migrate from codeigniter because composer and every other stuff, but somehow i crossed that too, but i'm now confused with routing and other stuff and i've tried many tutorials but i'm still not able to see how can i migrate from application where i'm managing students, where they can change email, change phone number updated stuff, in codeigniter it was easy but i don't how to approach this stuff in routing of laravel, now this question sounds way to dumb for community who is already working on laravel but if you see from my point of view it is going to affect my bread and butter. This is how i use to approach in codeigniter
class Student extends CI_Controller{
// usual piece of code of constructor
function update_email()
{
// piece of code to update email
}
}
but now with laravel routing system and all i've no idea how to approach this a resource controller looks like this
<?php
class StudentController extends \BaseController {
/**
* Display a listing of the resource.
*
* #return Response
*/
public function index()
{
}
/**
* Show the form for creating a new resource.
*
* #return Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* #return Response
*/
public function store()
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
* #return Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* #param int $id
* #return Response
*/
public function update($id)
{
//
}
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return Response
*/
public function destroy($id)
{
//
}
}
now every part is okay but how can i approach things where i've to update only email address or only phone number and stuff
Actually, in your question you have a RESTful controller which could be confusing for you at this time because you are new to Laravel and used CI, so probably you are very so much used with automating URL mapping without route. In this case, for ease, I suggest you to use a plain controller and that is almost same thing that you've did in CI but here in Laravel you have to declare route for every action. So, just create two routes like these:
Rouite::get('something/edit/{id}', array('uses' => 'StudentController#edit', 'as' => 'edit.record'));
Rouite::post('something/update/{id}', array('uses' => 'StudentController#update', 'as' => 'update.record'));
Then create a class/Controller and declare these methods:
class StudentController extends baseController {
// URL: http://example.com/something/edit/10 and it'll listen to GET request
public function edit($id) {
// $record = Load the record from database using the $id/10
// retuen View::make('editform')->with('record', $record);
}
// URL: http://example.com/something/update/10 and it'll listen to POST request
public function update($id) {
// Update the record using the $id/10
}
}
In your form, you need to use http://example.com/something/update/10 as action and you may use route('update.record') or url('something/update/10') to generate the action in the form. Read more on Laravel Documentation.
What you have created (or maybe rather generated) here is called Restful controller. It is some 'standard way' to manage CRUD actions (create/read/update/delete). However there is no need to do it this way. You can add in your controller whatever methods you want and not use Restful Controllers at all. That's your choice.
You can create in your function new method
function updateEmail()
{
// do here whatever you want
}
and in your routes.php file add new route:
Route::match(array('GET', 'POST'), '/changegemail', 'StudentController#updateEmail');
Now you can write your code for this method and it will work.
I need to cache some info about a user who is logged in (such as security groups, name, username, etc.)
Currently I have a separate class to achieve just this, lets call it CurrentUserHelper. Given a user object, it will cache the appropriate data and save store info in the $_SESSION variable.
The issue is I'm finding a bunch of classes relying just on CurrentUserHelper because they only need a couple of common fields. In fact, most of the functions have the same name as my User class. There's a couple of functions in CurrentUserHelper, such as getSecurityGroupsNames(), that contains a cache of all security group names, but there is no reason this function name could not be in the user class also.
Instead, should I create a CachedUser class and pass this around? This class can extend User, but then I can override getName(), getSecurityGroups(), etc, and returned the cached data, and not preform db requests to get the data.
The downside of passing around a CachedUser object is that this kind of hides the fact the data isn't really up to date if a constructor/function is accepting a type User. I also need to find way to handle merging the entity with Doctrine 2, and making sure entities associating themselves with a CachedUser won't break. If I decide to cache some temporary data (such as # of page views since logged in), this property shouldn't be part of the User class, it's more about the current user's session.
If I continue using the CurrentUserHelper class, maybe I should create an interface and have both CurrentUserHelper and User for the common functionality the two classes would share?
Preface: Extension isn't the best way for these sorts of things.. I'm sure you've heard composition over inheritance shouted at you over and over again. In fact, you can even gain inheritance without using extends!
This sounds like a good use-case for the decorator pattern. Basically, you wrap your existing object with another one that implements the same interface, so it has the same methods as the inner object, but this object's method adds the extra stuff around the method call to the inner object, like caching, for example.
Imagine you have a UserRepository:
/**
* Represents an object capable of providing a user from persistence
*/
interface UserProvider
{
/**
* #param int $id
*
* #return User
*/
public function findUser($id);
}
/**
* Our object that already does the stuff we want to do, without caching
*/
class UserRepository implements UserProvider
{
/**
* #var DbAbstraction
*/
protected $db;
/**
* #var UserMapper
*/
protected $mapper;
/**
* #param DbAbstraction $db
* #param UserMapper $mapper
*/
public function __construct(DbAbstraction $db, UserMapper $mapper)
{
$this->db = $db;
$this->mapper = $mapper;
}
/**
* {#inheritDoc}
*/
public function findUser($id)
{
$data = $this->db->find(['id' => $id]);
/** Data mapper pattern - map data to the User object **/
$user = $this->mapper->map($data);
return $user;
}
}
The above is a really simple example. It'll retrieve the user data from it's persistence (a database, filesystem, whatever), map that data to an actual User object, then return it.
Great, so now we want to add caching. Where should we do this, within the UserRepository? No, because it's not the responsibility of the repository to perform caching, even if you Dependency Inject a caching object (or even a logger!).. this is where the decorator pattern would come in useful.
/**
* Decorates the UserRepository to provide caching
*/
class CachedUserRepository implements UserProvider
{
/**
* #var UserRepository
*/
protected $repo;
/**
* #var CachingImpl
*/
protected $cache;
/**
* #param UserRepository $repo
*/
public function __construct(UserRepository $repo, CachingImpl $cache)
{
$this->repo = $repo;
$this->cache = $cache;
}
/**
* {#inheritDoc}
*
* So, because this class also implements UserProvider, it has to
* have the same method in the interface. We FORWARD the call to
* the ACTUAL user provider, but put caching AROUND it...
*/
public function findUser($id)
{
/** Has this been cached? **/
if ($this->cache->hasKey($id))
{
/**
* Returns your user object, or maps data or whatever
*/
return $this->cache->get($id);
}
/** Hasn't been cached, forward the call to our user repository **/
$user = $this->repo->findUser($id);
/** Store the user in the cache for next time **/
$this->cache->add($id, $user);
return $user;
}
}
Very simply, we've wrapped the original object and method call with some additional caching functionality. The cool thing about this is that, not only can you switch out this cached version for the non-cached version at any time (because they both rely on the same interface), but you can remove the caching completely as a result, just by changing how you instantiate this object (you could take a look at the factory pattern for that, and even decide which factory (abstract factory?) depending on a configuration variable).