I have recently dived into OOP & PHP MVC Application Design. At the moment I am learning a lot but I have one thing that is currently bugging me.
I read and now understand why it isn't wise to place http redirects within a service layer. We do not know what the controller will need to do once the service is complete, etc. etc. I also read that the service should not do anything outside of its purpose. Example: User Registration should only create a new user, using input passed by controller, but I am wondering if it is also fine to set flash messages within the service layer.
My application displays a lot of flash messages session based notifications for users. All of them are based on service related input validation checks, and produce alerts similar to the following
The username xxxxxx is already in use
Usernames Should be > 5 Characters
Should/can this be defined/set within the service class or is there something wrong with that? I have a Alert Helper function that handles setting the alerts. I can easily use my dependency injector to make it available I am just wondering if there is an issue with doing that.
I made the mistake of implementing all redirects within the services and I just finished removing all of them and placing them in the controllers, I don't want to make the same time consuming mistake so I am looking for advice here.
Thank you in advance for the help.
EDIT - CODE EXAMPLE
<?php
/**
*-----------------------------------------------------------------
*
* LOGIN CONTROLLER
*
*/
namespace Controller\www;
use \Helper\Controller;
class Login extends Controller {
public $dependencies = ['arena', 'login', 'site'];
/**
* Login
*
* Login Runs Through Various Checks Including If User is Banned, Account is Locked,
* or Forgot Password Request Is Active. Then the Entered Password is Matched & if Valid
* User is Logged In
*/
public function index() {
// Define Default
$username = '';
/**
* User Login
*
* If Successful, Login User, Redirect Home
* Else Set Error Alerts
*/
if ($this->form->post('login')) {
// Define and Sanitize Post Data
$username = $this->input->get('username');
$password = $this->input->get('password');
// Login Service Layer
$login = $this->factory->make('user/login');
// If Successful Redirect Home - Else Set Errors
if ($login->user($username, $password) === true) {
$this->redirect->home();
}
$this->alert->error($login->get('errors'));
}
/**
* Define Site Title & Display Page
*/
$this->view->sitetitle('login');
$this->view->display('www/login', [
'video' => $this->arena->video(),
'username' => $this->input->set($username)
], ['notifications' => 'user/forgotpassword']);
}
}
Service Layer
/**
*-----------------------------------------------------------------
*
* USER LOGIN SERVICE LAYER
*
*/
namespace Service\User;
use \Helper\Service;
class Login extends Service {
public $dependencies = ['login', 'mail', 'time', 'user', 'vars'];
/**
* Handles Entire Login Process For Site Users
*
* #params all User Submitted Form Data
*/
public function user($username = '', $password = '') {
// Validate $_POST Form Data
$this->validateInput($username, $password);
/**
* No Errors Produced - Complete Form Submission
*
* We Are Not Using `elseif` Between Forgot Password & Normal Login
* After a Forgot Password Code is Generated User May Remember Old Passwords
* We Need to Ensure Users Can Still Login Using Account Password As Well
*/
if (!$this->errors()) {
/**
* User Input Password Matches Account Password
*/
if ($this->input->verifyhash($password, $this->user->get('info.password'))) {
$this->login->user();
return true;
}
/**
* If We Have Not Been Redirected Login Was Unsuccessful
*/
$message = $forgotPW ? 'Forgot Password Code Invalid - Login Lost Incorrect' : 'Login Unsuccessful - Incorrect Username or Password';
$this->log->error($message, ['Username' => $username, 'Password' => $password]);
$this->error('Incorrect Username or Password');
}
/**
* If We Have Made It This Far Login Was Unsuccessful - Log Unsuccessful Attempt
*/
$this->login->logAttempt();
return false;
}
/**
* Validate $_POST Data
*
* #params all User Submitted Form Data
*/
private function validateInput($username = '', $password = '') {
// Display Error if Username is Empty
if (!$username) {
$this->error('Please enter a username');
}
// Display Error if Password is Empty
elseif (!$password) {
$this->error('Please enter a password');
}
// Search DB For User With Matching Username - If User Not Found Display/Log Error, Else Set User
else {
$user = $this->user->info($username, 'username', '', '`userid`');
if (!$user) {
$this->error('The username ' . $username . ' does not exist');
$this->log->error('User Not Found When Attempting to Login', ['username' => $username]);
} else {
$this->user->set('user', $user['userid']);
}
}
}
}
In order to answer your question, I think it's best to break down the concept of MVC into a very basic form, and its individual parts. I apologise in advance if this comes across as being somewhat condescending.
View
The view of the application displays anything and everything. If something is going to be displayed, it should be done in this layer
Controller
The controller is a mediator between the view and the model. It takes input from the view, applies logic/rules to it (where required), and interacts with the model to then get data to pass back to the view.
Model
This is where the loading and saving of data are done. The majority of the validation should have been done as part of the rules in the controller, and this should only pass details of any errors during loading or saving back to the controller should the arise. If there are no errors, it should return the relevant data, or a success status back to the controller.
With those points in mind, the model should not set flash messages to the session, that should be done within the controller depending on the result from the model.
Look at redirects and alerts as specific to one particular form of UI, and it should be obvious that there's no place for them in the Model. Simply always try to picture an alternative interface for your application; e.g. a command line interface for administrative tasks or a REST API. Redirects obviously have no place in either of these alternatives. Alerts are debatable... at the very least the form of the alert will be very different. Your Model will need to be able to pass back some status code to your Controller or View, and then it's the job of the Controller to react to "negative" events and the job of the View to visualise any alerts if necessary.
For example, your model may do something like this:
public function registerUser(User $user) {
...
if (!$successful) {
throw new EmailAlreadyRegisteredException;
}
return true;
}
The controller may then look like this:
public function userRegistration(Request $request) {
try {
$user = User::fromRequest($request);
$this->services->get('Users')->registerUser($user);
$this->view->render('registration_successful', $user);
} catch (InvalidUserData $e) {
$this->view->render('registration_form', $request, $e);
} catch (EmailAlreadyRegisteredException $e) {
$this->view->render('registration_failed', $user, $e);
}
}
The "alert" is passed around as an exception. It's just a method for the Model to signal to its callers what happened. It's up to the callers then to react to and visualise those events. You should certainly not expect any particular type of visualisation in the Model. So you don't want to hardcode specific HTML encoded messages or such. You don't even want to touch human languages at all, that's all the job of the View.
Related
I've searched around and can't seem to find a solution to the problem. I'm a rookie developer, so apologies if this is straight forward.
I'm wanting to have a simple re-direct depending on the user role. I have a "role" row within my "Users" table, and I want them to be directed to the "Index.php" page if they are a "user", and the "Dashboard" page if they are an "administrator".
I understand that it has something to do with the "SiteController", I'm just not sure of the exact code. For a reference, I currently have the following under the "ActionLogin" function -
public function actionLogin()
{
$model=new LoginForm;
// if it is ajax validation request
if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
{
echo CActiveForm::validate($model);
Yii::app()->end();
}
// collect user input data
if(isset($_POST['LoginForm']))
{
$model->attributes=$_POST['LoginForm'];
// validate user input and redirect to the previous page if valid
if($model->validate() && $model->login())
$this->redirect(array("Site/Dashboard"));
}
// display the login form
$this->render('login',array('model'=>$model));
}
Does anybody know how to do this?
Thanks a lot, I'm slowly learning!
In order to implement role base access you have to exted the default implementation of Yii, which comes only with user authentication (user is logged or user is guest).
In order to start with role based access, I recommend you to start by implementing your user class by extending the Yii CWebUser class.
Something like:
class WebUser extends CWebUser {
/**
* cache for the logged in User active record
* #return User
*/
private $_user;
/**
* is the user a superadmin ?
* #return boolean
*/
function getIsSuperAdmin(){
return ( $this->user && $this->user->accessLevel == User::LEVEL_SUPERADMIN );
}
/**
* is the user an administrator ?
* #return boolean
*/
function getIsAdmin(){
return ( $this->user && $this->user->accessLevel >= User::LEVEL_ADMIN );
}
/**
* get the logged user
* #return User|null the user active record or null if user is guest
*/
function getUser(){
if( $this->isGuest )
return null;
if( $this->_user === null ){
$this->_user = User::model()->findByPk( $this->id );
}
return $this->_user;
}
}
As you can see User::LEVEL_SUPERADMIN and User::LEVEL_ADMIN are provided by CWebUser. Then in your site controller accessRules() put something like:
// Get the current user
$user = Yii::app()->user;
function accessRules(){
return array(
//only accessable by admins
array('allow',
'expression'=>'$user->isAdmin',
),
//deny all other users
array('deny',
'users'=>array('*').
),
);
}
In order to use your new class with role based access, add it in the config/main.php file as an application component:
'components'=>array(
'user'=>array(
//tell the application to use your WebUser class
'class'=>'WebUser'
),
),
In your views, you can see how it works by using:
if(Yii::app()->user->isAdmin){
echo 'Administrator!';
}
if(Yii::app()->user->isSuperAdmin){
echo 'SuperAdmin!';
}
You have to manage the database table for users, and maybe add fields to store the user role constant. Further readings on Role Base Access are:
Yii Authentication
Simple Auth Sistem
Simple RBAC
To continue reading about the code provided in answer, go here.
Update
In order to perform the redirect as you mention, try:
// collect user input data
if(isset($_POST['LoginForm'])) {
$model->attributes=$_POST['LoginForm'];
// validate user input and redirect to the previous page if valid
if($model->validate() && $model->login())
// If you just want to run the view
$this->render('dashboard',array('model'=>$model));
// If you want to reander the action inside the controller
// $this->redirect( array("site/dashboard") );
}
// display the login form
$this->render('login',array('model'=>$model));
}
Note that dashboard.php file must be placed inside /protected/views/site folder.
I am using the Laravel 5 built in user stuff with Entrust for user roles and permissions. I have two roles set up which are administrators and users. Basically what I want to do is have two different forgotten password email templates - one for users and one for administrators. So when a user enters their email address to get the reset link emailed to them I need to check what sort of user they are first and then send them the right template. I don't want to have to do any sort of hacky stuff in the standard email template their must be a way to do this in the controller or something surely? Anyone know how I would do it?
You can probably prompt them to enter their email and when they submit you can grab it in the controller:
public function forgot()
{
$email = Input::get('email');
$user = User::where('email', $email)->first();
if($user->type == 'admin') {
// logic to email admin with admin template
} else {
// logic to email user with user template
}
// redirect to success page letting user know the email was sent
return View::make('someview');
}
Or better yet, just pass the user type to an email service that handles the emailing:
public function forgot()
{
$email = Input::get('email');
$user = User::where('email', $email)->first();
$emailService->sendForgotForType($user->type);
// redirect to success page letting user know the email was sent
return View::make('someview');
}
If you are using Laravel 5's built in User Management:
To override the default template used you would need to manually set the $emailView in the PasswordBroker.php by writing a new class that extends PasswordBroker.
For example, comment out 'Illuminate\Auth\Passwords\PasswordResetServiceProvider' in config/app.php
Then create an extension class:
use Illuminate\Contracts\Auth\PasswordBroker;
use Illuminate\Contracts\Auth\CanResetPassword;
class MyPasswordBroker extends PasswordBroker {
// override
public function emailResetLink(CanResetPasswordContract $user, $token, Closure $callback = null)
{
// Override Logic to email reset link function perhaps using the example above?
}
}
Then you would need to bind your new MyPasswordBroker class to AppServiceProvider at app/Providers/AppServiceProvider.php in the register method (below found online):
$this->app->bind('App\Model\PasswordBroker', function($app) {
$key = $app['config']['app.key'];
$userToken = new \App\Model\NewUserToken;
$tokens = new \App\Repository\NewTokenRepository($key,$userToken);
$user = new \App\Model\NewUser;
$view = $app['config']['auth.password.email'];
return new \App\Model\PasswordBroker($tokens, $users, $app['mailer'], $view);
});
Definitely moderately advanced stuff, if you can handle it - great. Otherwise I would possibly look into using an authentication package with built in features you need.
I wanted to create a dynamic signup.php. The algorithm is as follow:
Algorithm
when signup.php is requested by client, the code will attempt to check whether user send any data in $_POST.
if $_POST does not contains any data (means it's the first time user request for signup.php), a signup form will be return to the user, allowing user to enter all his/her details and again send back to signup.php through submit button.
if $_POST does contains data (means user has fill up the signup form and is now sending all the data back to signup.php), then the php code will attempt validate all those data and return result showing user has been successfully registered or error if failed to do so.
The problem I'm having right now is how am I going to check whether it's the first time user request for signup.php or not?
Use isset() to check if $_POST contains data.
http://php.net/isset
To answer your question, "how am I going to check whether it's the first time user request for signup.php or not?", honestly, probably for other users......
There are a few ways, cookies, storing request ips in a database, bleh, bleh, bleh. But...... None of them are guaranteed. The user can disable cookies, use a dynamic ip, etc. You could issue a unique hash and place it as a login.php?q=encValueForUniquePageRequest
but...... The architecture you laid out won't be practical.
Sorry :(
To check that request is POST:
<?php
if($_SERVER['REQUEST_METHOD']=='POST'){
//process new user
}
?>
Example:
<?php
Class signup_controller extends controller{
private $data = array();
private $model = array();
function __construct(Core $core){
parent::__construct($core);
/* load models - assign to model */
$this->model['page'] = $this->core->model->load('page_model', $this->core);
$this->model['auth'] = $this->core->model->load('auth_model', $this->core);
/* check script is installed - redirect */
if(empty($this->core->settings->installed)){
exit(header('Location: '.SITE_URL.'/setup'));
}
}
function index(){
/* do signup - assign error */
if($_SERVER['REQUEST_METHOD'] == 'POST'){
if($this->model['auth']->create_user(1)===false){
$this->data['error'] = $this->model['auth']->auth->error;
}
}
/* not logged in */
if(empty($_SESSION['logged_in'])){
/* assign form keys */
$_SESSION['csrf'] = sha1(uniqid().(microtime(true)+1));
$_SESSION['userParam'] = sha1(uniqid().(microtime(true)+2));
$_SESSION['passParam'] = sha1(uniqid().(microtime(true)+3));
$_SESSION['emailParam'] = sha1(uniqid().(microtime(true)+4));
/* get partial views - assign to data */
$this->data['content_main'] = $this->core->template->loadPartial('partials/signup', null, $this->data);
$this->data['content_side'] = $this->core->template->loadPartial('about/content_side', null, $this->data);
/* layout view - assign to template */
$this->core->template->loadView('layouts/2col', 'content', $this->data);
}
/* signed in - redirect */
else{
exit(header('Location: ./user'));
}
}
}
?>
I have one user class which consists of two types of users and want to allow different users to go to different pages.
I have created a filter as follows
Route::filter('isExpert', function()
{
$userIsExpert = 0;
$userIsLoggedIn = Auth::check();
if ($userIsLoggedIn && Auth::user()->role == 'expert') {
$userIsExpert = 1;
}
Log::info('Logged in: ' . $userIsLoggedIn . ' && Expert: ' . $userIsExpert);
if ($userIsExpert == 0)
{
Log::info('should be logging out now.');
Auth::logout();
return Auth::basic();
}
});
And routing like so
Route::get('/winners', array('before' => 'isExpert', function()
{
$winners = DB::select('select * from winners');
return View::make('winners.index')->with('winners',$winners);
}));
The thought is this: If it's not an expert, it will logout and redirect to login page. If it is, it will simply continue.
However, Auth::logout(); doesn't ever log out the user.
Question
Why is not Auth::logout() working? I've tried placing it anywhere in the app to no avail.
cheers
I had the same problem, I really couldn't logout the current user... And the answer is simple: Laravel doesn't support logout() with Auth::basic().
There are ways to fix it, but it's not very clean; https://www.google.nl/search?q=logout+basic
This is not a limitation to Laravel, HTTP Basic Authorization is not designed to handle logging out. The client will remain logged in until the browser is closed.
HTTP Basic Authorization really shouldn't be used in any public production environment. Here are some reasons why:
No way to give users a "remember me"-option on the login form.
Password managers have no or lacking support for HTTP Basic Auth, as it is not rendered HTML but a native popup.
Terrible user experience. Putting together a proper login form is well worth the little time it takes.
The only valid case I can think of is to protect public development-subdomains like dev.example.com, but there are better ways to solve that as well.
The easiest way that I've found for that is to redirect to invalid username/password on logout route. Example:
Route::get('admin/logout', function() {
return Redirect::to(preg_replace("/:\/\//", "://log-me-out:fake-pwd#", url('admin/logout')));
});
If you implemented these methods in User.php
/**
* Get the e-mail address where password reminders are sent.
*
* #return string
*/
public function getReminderEmail()
{
return $this->email;
}
public function getRememberToken()
{
return $this->remember_token;
}
public function setRememberToken($value)
{
$this->remember_token = $value;
}
public function getRememberTokenName()
{
return 'remember_token';
}
add new column with name 'remember_token' to your table 'users' in mysql database, and then log out, finally it solved successfully.
to alternate you table use this SQL Command:
ALTER TABLE users ADD remember_token TEXT;
and then press 'Go' button.
I am having a web project where some access decisions are dependant on the page itself (e.g. /logout which shall only be visible to logged in users) and some are dependant on dynamic model objects (e.g. /article/delete/1234 where we have to check if 1234 was written by the logged in user or if he is an admin).
Now, I am facing the problem of how to bring both things together. No matter how I tried, I cannot rely on any of the two alone:
Some pages do not use any models, so I cannot setup any model rules there
On the other hand, I cannot create dynamic assertions for a modular approach, because Comment is just a comment and not a default/comment. A Comment is not restricted to the default module, it may also be used in the admin module.
With modular ACL I am trying to check for each page if a user is allowed to visit it, e.g.
if (!$acl->isAllowed($user, 'default/secrets', 'mysecrets')) {
$this->forward('auth', 'login');
$this->setDispatched(false);
}
And with dynamic assertions I am checking if somebody is allowed to edit a specific model object.
// $comment has a method getResourceId() returning 'comment'
if ($acl->isAllowed($user, $comment, 'delete')) {
// display a link for deletion
}
Of course it would be nice if the check for
deletion of a specific comment, and
accessing the /comment/delete/???? page
would be the same, but I guess this is not possible and I would have to create two rules:
$acl->allow('member', 'default/comment', 'delete');
$acl->allow('member', 'comment', 'delete', new Acl_Assert_CommentAuthor());
$acl->allow('admin', 'comment', 'delete');
Now, this seems not perfect to me as this can lead to duplicate work in some cases.
Is there some better method to approach this problem? Or is the only method to at least create a coherent naming scheme like: mvc:default/comment, model:comment
The way i did it, custom sql queries that restrict results, functions that check before insert/delete/modify sql
and then an ACL plugin i wrote that checks permission and uses these 5 tables
acl_groups
acl_permession
acl_permession_groups
acl_permession_risource
acl_risource
Code:
class Abc_Controller_Plugin_Acl extends Zend_Controller_Plugin_Abstract
{
/**
* Return whether a given request (module-controller-action) exists
*
* #param Zend_Controller_Request_Abstract $request Request to check
* #return boolean Whether the action exists
*/
private function _actionExists(Zend_Controller_Request_Abstract $request)
{
$dispatcher = Zend_Controller_Front::getInstance()->getDispatcher();
// Check controller
if (!$dispatcher->isDispatchable($request)) {
return false;
}
$class = $dispatcher->loadClass($dispatcher->getControllerClass($request));
$method = $dispatcher->formatActionName($request->getActionName());
return is_callable(array($class, $method));
}
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
// fetch the current user
$controller = $request->controller;
$action = $request->action;
$logger = Zend_Registry::get('log');
//prima controlla se sei autenticato e poi controlla se l'azione esiste, cosi non esponi il sito
$auth = Zend_Auth::getInstance(); /* #var $auth Zend_Auth */
if($auth->hasIdentity()) {
if(! $this->_actionExists($request))//l'azione non esiste?
{
$request->setControllerName('error');
$request->setActionName('pagenotfound');
$logger->notice( " IP: ". $_SERVER['REMOTE_ADDR']. " http://".$_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]. " ?" .http_build_query($_REQUEST));
return ;
}
$allowed = Abc_Action_Helper_CheckPermission::checkPermission($controller, $action);
if ($allowed !== 1)
{
$request->setControllerName('error');
$request->setActionName('noauth');
}
//fine azione esiste
}else{
$request->setControllerName('user');
$request->setActionName('login');
return ;
}
}//fine preDispatch
}
You can then add your code(which i ommited for shortness) to remember the request and redirect you there after login.