Introducción práctica a Laravel: Tutorial para sistema de usuarios

Laravel: You have arrived.

De algo de tiempo para acá Laravel ha empezado a hacerse realmente popular dentro del mundo de los frameworks en PHP. Esto se debe a la accesibilidad para usarlo ya que es funcional prácticamente después de instalarlo permitiendo construir de manera rápida una aplicación estándar. Además, al utilizar componentes que han probado su robustez (Oh gran Symfony) en lugar de reinventar la rueda, Laravel ofrece un entorno de desarrollo sencillo montado sobre bases sólidas.

Normalmente los tutoriales para aprender a usar un framework se enfocan en la creación del obligado blog. Evidentemente necesitaremos un mecanismo de acceso para algo así de modo que esta guía contiene las instrucciones para crear el sistema de logueo y registro que podremos usar más adelante al crear nuestro blog, CMS, o cualquier otra aplicación.

Si has empezado a leer las introducciones en la documentación oficial verás que desde el inicio te envían a crear rutas, generar alguna vista y ver resultados. Más rápido no podría ser, sin embargo si ya has usado otros frameworks todo esto ya empezará a sonar algo raro. No temas, Laravel 4 permite nativamente la creación de paquetes (llamados anteriormente bundles) para mantener nuestro código con el acoplamiento y cohesión adecuados.

Para que nuestro primer paquete sea realmente útil en un futuro lo implementaremos usando una gran librería para la tarea: Cartalyst Sentry. Sentry implementa de muy buena manera un sistema de usuarios. Aún cuando en este tutorial no haremos nada avanzado, nos facilitará implementar mejoras conforme nuestras necesidades lo requieran (grupos, permisos, bloqueos, etc.).

Nota: Se da por hecho que tienes un entorno listo: servidor local, PHP>=5.4, RDBMS, Composer y Laravel accesible desde tu navegador (You have arrived.). Puedes seguir la guía de instalación para trabajar con Vagrant y Homestead, que es una máquina virtual con todo lo necesario para Laravel.

Antes de comenzar necesitamos configurar el creador de paquetes editando …/app/config/workbench.php agregando nombre y dirección electrónica, y el acceso a la base de datos que usará el framework, editando …/app/config/database.php.

Para trabajar en Homestead lo siguiente puede ser útil:

		'mysql' => array(
			'driver'    => 'mysql',
			'host'      => '127.0.0.1',
			'port'      => 33060,
			'database'  => 'homestead',
			'username'  => 'homestead',
			'password'  => 'secret',
			'charset'   => 'utf8',
			'collation' => 'utf8_unicode_ci',
			'prefix'    => '',
		),

	mysql -h127.0.0.1 homestead -P33060 -uhomestead -p

Ya que tenemos configurado el acceso a nuestra base, la prepararemos para migrar (crear tablas e importar datos), esto lo haremos ejectuando:

php artisan migrate:install

que nos creará la tabla de control de migraciones.

Comenzaremos creando el esqueleto de nuestro paquete de autenticación:

php artisan workbench allegro/auth

Ejecutando la línea anterior desde la terminal en la ruta base donde está Laravel, artisan creará una estructura estandar para un paquete. Si revisamos el directorio raiz de Laravel veremos la siguiente estructura de directorios creada:

laravel_root
    ...
    workbench/
        allegro/
            auth/
                ...

Dentro de workbench se encontrarán todos los paquetes que hagamos. allegro es el vendor que puedes/debes cambiarlo por tu nickname, usuario de Git, compañía, palabra impactante, etc; auth es el paquete en específico que estamos creando.

Si revisas el contenido en …/workbench/allegro/auth/ verás que la estructura del paquete es realmente sencilla, más adelante iremos agregando algunas cosas.

Lo primero que haremos será configurar el composer.json de nuestro paquete:

{
    "name": "allegro/auth",
    "description": "A simple authentication implementation using Sentry",
    "license": "MIT",
    "authors": [
        {
            "name": "John Doe",
            "email": "john.doe@domain.com"
        }
    ],
    "require": {
        "php": ">=5.4.0",
        "illuminate/support": "4.2.*",
        "cartalyst/sentry": "2.1.*"
    },
    "autoload": {
        "psr-0": {
            "Allegro\\Auth": "src/"
        }
    },
    "minimum-stability": "stable"
}

Agregamos una licencia, una descripción e indicamos que vamos a utilizar el paquete Sentry. Luego de guardar los cambios actualizaremos las dependencias ejecutando en el directorio raiz de nuestro paquete (…/workbench/allegro/auth/)

composer update

Una vez que se haya descargado Sentry publicamos su configuración en Laravel y creamos la base de datos.

Para la parte de configuración abriremos el archivo …/app/config/app.php y agregaremos el nombre de la clase de Sentry al arreglo de proveedores y su alias al arreglo de aliases:

	'providers' => array(
		...
		'Cartalyst\Sentry\SentryServiceProvider',
	...
	'aliases' => array(
		...
		'Sentry'          => 'Cartalyst\Sentry\Facades\Laravel\Sentry',

después debemos exponer el archivo de configuración. Como estamos agregando la dependencia a un paquete y no directamente al framework debemos definir explícitamente su ubicación.

php artisan config:publish --path="workbench/allegro/auth/vendor/cartalyst/sentry/src/config" cartalyst/sentry

Ya sólo queda generar las tablas en la base:

php artisan migrate --path="workbench/allegro/auth/vendor/cartalyst/sentry/src/migrations/" --force

Con lo anterior tenemos la dependencia lista para ser usada, de lujo eh. Vayamos ahora a configurar nuestro paquete.

Primero vamos a editar la clase …/workbench/allegro/auth/src/Allegro/Auth/AuthServiceProvider.php que es la clase de arranque agregando el método boot():

    ...
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
        $this->package('allegro/auth');
        include __DIR__.'/../../routes.php';
    }
    ...
}

La primera línea en boot() establece como identificará el framework nuestro paquete, la segunda carga el archivo (inexistente hasta este momento) que contendrá la definición de nuestras rutas.

El paquete ya puede ser cargado por Laravel y sólo falta registrarlo en él para activarlo, esto se hace abriendo el archivo …/app/config/app.php y agregando al arreglo de proveedores como lo hicimos con Sentry:

	'providers' => array(
		...
		'Cartalyst\Sentry\SentryServiceProvider',
		'Allegro\Auth\AuthServiceProvider',

Ahora vamos a crear el archivo de rutas routes.php dentro de la raiz de nuestro paquete (…/workbench/allegro/auth) y definir las rutas de acceso al paquete. Esto nos dará una visión general de que tiene que existir.

 'user_login_show',
    'uses'       => "$sessionController@create"
]);

Route::post('login', [
    'as'         => 'user_login_action',
    'uses'       => "$sessionController@store"
]);

Route::get('logout', [
    'as'         => 'user_logout',
    'uses'       => "$sessionController@destroy"
]);

// // // // // // // // // // // // // // // // // // // // // // // User routes

Route::get('register', [
    'as'         => 'user_register_show',
    'uses'       => "$userController@create"
]);

Route::post('register', [
    'as'         => 'user_register_action',
    'uses'       => "$userController@store"
]);

Route::get('user/edit', [
    'before'     => 'sentry.auth',
    'as'         => 'user_edit_show',
    'uses'       => "$userController@edit"
]);

Route::post('user/edit', [
    'before'     => 'sentry.auth',
    'as'         => 'user_edit_action',
    'uses'       => "$userController@update"
]);

Route::get('user/delete', [
    'before'     => 'sentry.auth',
    'as'         => 'user_delete',
    'uses'       => "$userController@destroy"
]);

Si has leído un poco de enrutamiento en Laravel sabrás que hay diferentes estrategias disponibles para acceder a los controladores, sin embargo las estrategias automáticas (REST, recursos) limitan el manejo de las rutas al usar enrutamientos inversos y hacen menos evidentes los accesos a nuestro sistema, así que usaremos el método básico para definir explícitamente todos nuestros accesos y tener un control total, de cualquier manera usaremos los nombres estándar para las acciones.

Personalmente uso namespaces siempre que puedo. Si no los usas hay buenas razones para empezar a hacerlo, aunque esto significa que necesitaremos agregar \ como prefijo a cada fachada usada para evitar que sea interpretada como parte de nuestro paquete (ej. \BaseController:: en lugar de BaseController::, \Input:: en lugar de Input::, etc). Siendo consistentes también con la propuesta de uso de namespaces que en AuthServiceProvider.php usaremos controladores con namespaces.

Podríamos también realizar el trabajo de los controladores dentro de las propias funciones de enrutamiento agregando clousures, sin embargo por razones de separación de concernimientos usaremos routes.php para definir los accessos y SessionController.php / UserController.php para la acciones correspondientes al acceso y gestión.

Ahora vamos a crear los controladores que procesarán las solicitudes que acabamos de definir.

php artisan controller:make --bench allegro/auth UserController --only=create,store,edit,update,destroy

creará el controlador de usuarios y

php artisan controller:make --bench allegro/auth SessionController --only=create,store,destroy

creará el controlador de sesiones.

Como no usaremos todas las acciones REST indicamos de manera explícita el subconjunto de métodos que nos interesa que sean maquetados. Nuestros nuevos controladores han sido creados y depositados dentro del (nuevo) subdirectorio controllers.

La clase UserController necesita el siguiente código:

 \Input::get('email'),
                'password'      => \Input::get('password'),
                'activated'     => true,
                'created_at'    => date('Y-m-d H:i:s'),
                'updated_at'    => date('Y-m-d H:i:s'),
            ]);
        }
        /* @var $ex \Exception */
        catch (\Exception $ex) {
            return \Redirect::route('user_register_show')
                ->withInput()
                ->with('error', $ex->getMessage());
        }

        return \Redirect::route('user_login_show')
                ->with('notice', 'User has been created');
    }


    /**
     * Show the form for editing the specified resource.
     *
     * @return Response
     */
    public function edit()
    {
        return \View::make('auth::user.edit');
    }


    /**
     * Update the specified resource in storage.
     *
     * @return Response
     */
    public function update()
    {
        try {
            /* @var $user \Cartalyst\Sentry\Users\Eloquent\User */
            $user = \Sentry::getUser();

            $user->first_name = \Input::get('first_name');
            $user->last_name = \Input::get('last_name');

            if ('' !== \Input::get('password') . \Input::get('password_r')) {
                $p1 = \Input::get('password');
                $p2 = \Input::get('password_r');
                if ($p1 !== $p2) {
                    throw new \Exception('Passwords don\'t match');
                }
                $user->password = $p1;
                $logout = true;
            }
            if (!$user->update()) {
                throw new \Exception('Unable to update values');
            }
        }
        /* @var $ex \Exception */
        catch (\Exception $ex) {
            return \Redirect::route('user_edit_show')
                ->withInput()
                ->with('error', $ex->getMessage());
        }

        if (isset($logout)) {
            \Sentry::logout();
            return \Redirect::route('user_login_show')
                    ->with('notice', 'User has been updated, please log in again');
        }

        return \Redirect::back()
                ->with('notice', 'User has been updated');
    }

    /**
     * Remove the specified resource from storage.
     *
     * @return Response
     */
    public function destroy()
    {
        /* @var $user \Cartalyst\Sentry\Users\Eloquent\User */
        $user = \Sentry::getUser();
        $user->delete();
        \Sentry::logout();
        return \Redirect::route('user_login_show')
                ->with('notice', 'User has been deleted. Wanna check?');
    }
}

y la clase SessionController:

 \Input::get('email'),
            'password'     => \Input::get('password'),
        ];
        $remember = \Input::get('remember') === '1';

        try {
            \Sentry::authenticate($credentials, $remember);
        }
        catch (\Exception $ex) {
            $msg = $ex instanceof \Cartalyst\Sentry\Throttling\UserSuspendedException
                    ? $ex->getMessage()
                    : 'Invalid credentials';

            return \Redirect::back()
                    ->withInput()
                    ->with('error', $msg);
        }

        // Extract requested page from session
        $redirect = \Session::get('loginRedirect', \URL::route('user_edit_show'));
        \Session::forget('loginRedirect');

        return \Redirect::to($redirect);
    }

    /**
    * Remove the specified resource from storage.
    *
    * @return Response
    */
    public function destroy()
    {
        \Sentry::logout();
        \Session::flash('notice', 'Session closed');
        return \Redirect::back();
    }

}

Algo a notar es que los parámetros que recibían algunos métodos se han removido pues no son útiles en nuestro caso.

Laravel usa Composer para la carga automática de clases, así que para que nuestros controladores sean usables debemos hacer dos cosas: agregar al composer.json del paquete la sección classmap con el directorio controllers y luego ejecutar desde la raiz de nuestro paquete el comando de actualización de carga automática de clase.

    "autoload": {
        "classmap": [
            "src/controllers"
        ],
        "psr-0": {
            "Allegro\\Auth": "src/"
        }
    },
composer dump-autoload

Nota: Si el proveedor de servicios no se puede encontrar, ejecuta el comando php artisan dump-autoload desde el directorio raiz de tu aplicación.

Con lo que hemos hecho hasta aquí ya tenemos el back-end prácticamente listo y sólo necesitamos crear algunas vistas para nuestro front-end.

El motor de plantillas de Laravel es bastante sencillo pero lo suficientemente potente para crear templates reutilizables y extensibles permitiéndonos reciclar código. Revisando los controladores vemos que tenemos algunas líneas como return \View::make('auth::user.register'); que podemos interpretar como «Hey amigo, carga la vista auth/views/user/register[.blade].php».

La estructura de plantillas que corresponde a los llamados de los controladores (y que debemos crear) es la siguiente:

auth/
    public/
        style.css
    src/
        views/
            base.blade.php
            user_box.blade.php
            session/
                login.blade.php
            user/
                edit.blade.php
                register.blade.php
    ...

base.blade.php contendrá la plantilla general para todas las vistas, excepto user_box.blade.php (que será importada por otros paquetes para mostrar una cajita que despliegue al usuario activo y ligas de inicio y cierre de sesión). Los demás archivos no necesitan más explicación.

base.blade.php:



    
        
        Userland
        
    
    
        @if(Session::has('error'))
            

{{ Session::get('error') }}

@endif @if(Session::has('notice'))

{{ Session::get('notice') }}

@endif
@yield('main')

user_box.blade.php:

@if (!Sentry::check()) Sign in @else {{ Sentry::getUSer()->getLogin() }} / Sign out @endif

register.blade.php:

@extends('auth::base')

@section('main')
{{ Form::open() }}
    
Register
{{ Form::label('email', 'Email') }}
{{ Form::text('email') }}
{{ Form::label('password', 'Password') }}
{{ Form::password('password') }}
{{ Form::label('password_r', 'Repeat pwd') }}
{{ Form::password('password_r') }}
Sign in   {{ Form::submit('Register', array('class' => 'btn')) }}
{{ Form::close() }} @stop

edit.blade.php:

@extends('auth::base')

@section('main')
format('D d, M Y, H:i');
    }
?>
{{ Form::open() }}
    
Edit
{{ Form::label('login', 'Login') }}
{{ Sentry::getUser()->getLogin() }}
{{ Form::label('firs_name', 'first name') }}
{{ Form::text('first_name', Sentry::getUser()['attributes']['first_name']) }}
{{ Form::label('last_name', 'last name') }}
{{ Form::text('last_name', Sentry::getUser()['attributes']['last_name']) }}
{{ Form::label('password', 'Password') }}
{{ Form::password('password') }}
{{ Form::label('password_r', 'Repeat pwd') }}
{{ Form::password('password_r') }}
Cancel   {{ Form::submit('Update', array('class' => 'btn')) }}

Registered at: {{ parseDate(Sentry::getUser()['attributes']['created_at']) }}

Last login: {{ parseDate(Sentry::getUser()['attributes']['last_login']) }}

Sign out


Delete account

{{ Form::close() }} @stop

login.blade.php:

@extends('auth::base')

@section('main')
    @if (!Sentry::check())
        {{ Form::open() }}
        
Sign in
{{ Form::label('email', 'Email') }}
{{ Form::text('email') }}
{{ Form::label('password', 'Password') }}
{{ Form::password('password') }}
{{ Form::label('remember', 'remember') }}
{{ Form::checkbox('remember') }}
{{ Form::close() }} @else
You already signed in as

{{ Sentry::getUSer()->getLogin() }}

Edit   Sign out
@endif @stop

style.css:

html {
    height: 100%;
}
body {
    background-color: #eee;
    margin: 0;
    padding: 0;
    font-family: sans-serif;
    height: 100%;
}
p.alert {
    text-align: center;
    font-weight: bold;
    padding: 10pt;
    margin: 0;
    background-color: #888;
    color: #fff;
}
p.alert-error {
    background-color: #ff3333;
}
.center-text {
    text-align: center;
}
.container {
}
#login-block {
    background-color: #f9f9f9;
    margin: 0 auto;
    padding: 10pt;
    max-width: 375px;
    border-radius: 0 0 10px 10px;
    border: 1px solid #ddd;
    border-top: 0;
}
.title {
    background: #85cf85;
    margin: -10pt -10pt 20pt -10pt;
    font-size: 200%;
    text-align: center;
}
.title > * {
    padding: 5pt;
}
.control-group {
    clear: both;
    margin: 10pt 0;
    overflow: hidden;
}
.label {
    float: left;
    vertical-align: middle;
}
.field {
    float:right;
    width: 65%;
    overflow: hidden;
    text-align: right;
}
.field > * {
    width: 97%;
    font-size: 100%;
    text-align: left;
}
.form-actions {
    text-align: right;
}
.btn {
    border-radius: 4pt;
    background: #ddd;
    color: #444;
    font-weight: bold;
    font-size: 100%;
    padding: 2px 8px;
}

Ahora solo resta hacer público nuestro archivo de estilos. Para eso nos vamos a nuestra consola y ejecutamos en la raiz de nuestro proyecto:

php artisan asset:publish --bench allegro/auth

Ahora si, tenemos todo listo.

Como has visto la creación de paquetes en Laravel no tiene complicaciones. Al ser éste un tutorial práctico introductorio, he tratado de mostrar una estructura adecuada y a la vez sencilla, que sirva de ejemplo y que además sea útil en un futuro y no sólo como un ejercicio, sin embargo hay algunas modificaciones que podrían venir bien, como mover el filtro que se encuentra en el controlador a un archivo de filtros y cargarlo junto con routes.php en la clase proveedora, o seccionar de mejor manera las vistas, etc.

El paquete completo puedes descargarlo desde Github

Feliz Laraveleo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *