Table of Contents
- Introduction
- Installation quest
- Conventions
- Base\Model
- Access control
- i18n
- CRUD
- Crud components
- Image uploading
- CSRF protection
- Admin sidebar configuration
- Generating bundles with cli
- Running tests
- Contributors
- License
Laravel Engine is a result of a constant evolution of a simple Laravel bundle which was originally written to make CRUD generation easier and faster.
With Laravel Engine you get a nice functionality to develop CRUD-oriented applications in a simple and fun way. It is like a LEGO for a progmrammer who just needs to glue all the "bricks" together in order to get a working application.
We call this bricks "Components" and there are two types of them in the Engine:
- Grid components - these are bricks for listing data stored in a database
- Form components - bricks that are used to add and edit data
To make this possible and easy to implement, we needed to improve most of the standard Laravel 3 libraries adding new functionality that made us write less code and get better results.
The most suiting way to describe all possibilities of the Engine is to write a book but we will try to make this README a good starting point for newbies.
Clone the Laravel Engine into a separate folder.
The best way to integrate the Engine with a Laravel 3 application is to create a symlink in the bundles/
directory:
$ cd bundles && ln -s path/to/Laravel/Engine/Mobileka/directory`
Or, if you are on Windows (Vista or newer):
cd bundles
mklink /D Mobileka path\to\Laravel\Engine\Mobileka
This allows you to get updates with a simple git pull
command in the Engine directory.
Please, make sure to .gitignore this folder in your main Laravel 3 project because it can potentially create problems with git.
The Engine consists of three big parts:
- Engine - core bundle which contains CRUD and other Laravel improvements
- Users - users and user groups which depend on Engine and typically are going to be overriden
- Auth - authentication / authorization which depends on Users
You need to register these bundles in the application/bundles.php
:
return array(
'engine' => array('location' => 'Mobileka/L3/Engine'),
'auth' => array('location' => 'Mobileka/L3/Auth', 'auto' => true),
'users' => array('location' => 'Mobileka/L3/Users', 'auto' => true),
);
Some of Laravel Engine components use composer packages, so you need to install it and integrate with Laravel 3:
-- Install composer: curl -sS https://getcomposer.org/installer | php
-- Add the following code to your app/start.php
file:
if (!File::exists('vendor/autoload.php'))
{
throw new Exception("You need to run composer update to complete installation of this project.");
}
require 'vendor/autoload.php';
-- Create composer.json
file in the root of your project and add these lines to it:
{
"require" : {
"intervention/image": "1.*",
"nesbot/Carbon": "*",
"ezyang/htmlpurifier": "dev-master",
"codeguy/upload": "*"
}
}
-- Run php composer.phar update
If you get the "allowed memory size exhausted" error try adding
-d memory_limit="1024M"
afterphp
in the above command
-- Add composer.lock
and /vendor/
to .gitignore
in the root of your application
Ok, lets continue. It is time to run migrations:
$ php artisan migrate:install
$ php artisan migrate
... add the following line on top of your application/routes.php
file:
Bundle::start('engine');
Create application/config/security.php
with the following content:
return array(
'admin_uri' => 'admin',
'admin_port' => false, //restrict access to admin interface by port, false to disable
'admin_ip' => false, //restrict access to admin interface by ip, false to disable
'allowed_login_attempts' => 5, //0 or false to disable this limit
'login_attempts_block_duration' => 15, //in minutes
/*
* if anybody doesn't use your web-application for a specefied below amount of days, his account will be deleted.
* false to disable
*/
'block_period' => 365,
);
... and add a route to handle the access to the administration interface:
Route::get(admin_uri(), array('as' => 'admin_home', 'uses' => 'users::admin.default@index'));
The admin_uri()
helper allows you to easily change the URI which handles access to admin interface (admin
is the default one).
To change this URI, you need to add admin_uri
parameter to the application/config/security.php
configuration file:
'admin_uri' => 'rulethesite'
Please note that the Engine requires every single route to have an alias. This means that other routes (including the default Laravel route) defined before integrating the Engine and not having an alias will break the application. In order to fix this, you either need to remove these routes or add an alias for all of them. We will discuss Laravel Engine routing more closely in an appropriate section.
The Engine contains a shitload of assets which must be published:
$ php artisan bundle:publish
In order Auth component to work properly, add these permissions to application/config/acl.php
:
<?php
return array(
'defaultResult' => false,
'allowedRoutes' => array(
'auth_admin_default_login',
'auth_admin_default_logout',
),
'permissions' => array(
'aliases' => array(
'(:any)_admin_(:any)' => array('admins'),
'admin_home' => array('admins'),
),
'paths' => array(),
),
'actions' => array(
'upload_files_without_restrictions' => array('admins')
),
);
and specify a model which maps the users
table application/config/auth.php
:
'model' => IoC::resolve('UserModel'),
If you are going to use an image uploading functionality, make sure to create a directory writable
by a webserver and specify it in paths.php
:
$paths['uploads'] = 'public/uploads';
Don't forget to create a .gitignore
file in this folder with the following contents:
*
!.gitignore
As you are probably going to use the ImageColumn
component, create application/config/image.php
file and add these lines to it:
<?php
return array(
'aliases' => array(
'multiupload_thumb' => array(99, 112), // Dimensions of thumbnails in multiupload
'admin_grid_thumb' => array(80, 80), // Dimensions of thumbnails in grid
),
'allowedFileTypes' => array('jpg', 'jpeg', 'png', 'gif') //modify for your needs
);
If you don't use SSL, you need to set these configuration parameters to false
:
ssl
inapplication.php
secure
insession.php
Use enviroment configuration files if these settings differ for your production server and local development machine
And you have finally finished the installation process! Now you should be able to go to http://sitename.dev/admin
(or http://sitename.dev/whatever_you_have_in_application_config_under_admin_uri_param
) to see the authorization form. Just in case you want to log in, these are the default credentials:
email: admin@example.com
password: 123456
-
Routing
- Route aliases
- RESTful urls (RestfulRouter class)
- .json and .ajax
-
Models
- naming
- saveData()
-
CRUD CRUD is the fundomental component of the Engine.
- Component configuration
- Language configuration
- Component value translation
- Events
- Field validation
- i18n field validation
- Image fields
- Structure
- Form
- Grid
- Form
- ImageField
- TextField
- ...
- DropdownChosenLinked
DropdownChosenLinked component exists for CRUD Form and Grid Filter. It allows you to create several selectboxes, values in which hierarchically depend on the values in the previous select boxes. For example, Country -> Region -> City -> Street -> Building. If we have a large number of buildings in the database, it is not rational to load them all in order to provide the user with thousands of options in a selectbox. It is not usable, and is very bad for performance, so you should use DropdownChosenLinked instead.
Using it is slightly harder than a normal DropdownChosen component, but nothing too hard.
First of all, make sure you have published all the assets of Laravel Engine. A new .js file was pushed to the repo recently, so run php artisan bundle:publish
if you haven't done that for a while.
Next, you will need to create a route for your ajax requests. Every time you select a value in one of the linked select boxes, an ajax request is sent in order to retrieve the selected element's children items, so we need to make sure we created the route for that. Just add this to your application/routes.php
file, and specify the models that you need to work with in the $possible_linked_items
array:
Route::get('admin/linked_list/(:any)/(:num)', array('as' => 'admin_linked_list', function($modelName, $id){
$result = array();
$possible_linked_items = array('Geo', 'Product_Type', 'Feature');
if (in_array($modelName, $possible_linked_items))
{
$model = IoC::resolve($modelName . 'Model');
$data = $model->getListByParent($id, 'name');
foreach ($data as $record)
{
$result[] = array(
'id' => $record->id,
'name' => $record->name,
);
}
return Response::json($result);
}
throw new Exception("Incorrect value for passed model: $modelName");
}));
Now all we need is to specify in the config.php
for our form or filter the linked items. It must be an array, with its keys being the fields in the database (which we are saving values for in the case of the Form, or which we are filtering by in the case of Filter) and the values being the model class of the children. In the following example I have three levels of product types, followed by one level of product features:
$linked_items = array(
'product_type_id_1' => 'Product_Type',
'product_type_id_2' => 'Product_Type',
'product_type_id_3' => 'Feature',
'feature_id' => 'Feature',
);
And then in the config of the actual fields:
'product_type_id_1' => formDropdownChosenLinked::make('product_type_id_1')->options($product_type_1s)->linked_items($linked_items),
'product_type_id_2' => formDropdownChosenLinked::make('product_type_id_2', array('disabled' => true))->options(array()),
'product_type_id_3' => formDropdownChosenLinked::make('product_type_id_3', array('disabled' => true))->options(array()),
'feature_id' => formDropdownChosenLinked::make('feature_id', array('disabled' => true))->options(array()),
We only need to fill the first selectbox with initial values, and the rest should be made disabled and empty. The component should do the rest to fill them when you are selecting a value in the first selectbox, or editing a record (all selectboxes will be filled and an appropriate option will be selected).
One last thing: as you can see in the route, the children are retrieved using a model's getListByParent() method. By default, its a simple method of getting the records with their parent_id
field equal to the specified value, but you can always override either the field (just specify static $parentField
class variable in your model) or the whole method in your model to make sure you only return the actual children.
- Grid
- ImageColumn
- TextColumn
- ...
- Filters
- StartsWithFilter
- DropdownFilter
- ...
Laravel Engine has a kind of a built-in possibility to upload images. To use this functionality you'll need to solve another difficult quest. But once you've done it, you'll get the following features:
- Standardized way to save and manipulate images (and other types of files)
- Asynchronous image uploading
- Easy way to crop and resize images (with possibility to do this dynamically)
- Multiuple image uploading (async, with previews, with possibility to remove them one by one and select a featured image)
- Built-in caching
- Other cool features that I forgot to mention
I wrote kind of, because this functionality depends on a composer package which you need to install before using image uploading:
$ php composer.phar require intervention/image 1.*
Lets start from a simple example when you just need to upload an image and bind it to your, say, Article object.
-
First, your Article model should extend
Mobileka\L3\Engine\Laravel\Base\ImageModel
(this is confusing and should be fixed). -
Then you need to enumerate image fields in your Article model like so:
public static $imageFields = array('img', 'another_image_field');
Please note: right now there is a naming problem and you should not call your field
image
because this crashes the system. Of course, this is going to be fixed some day
-
Now list all the accessible fields of the model (because this is a good practice and Image component will add fields which shouldn't be saved to the database with
fill()
method):public static $accessible = array('title', 'description');
-
To enable image uploading functionality, you need to create routes which handle uploading requests. But it is better to ask the RestfulRouter to do it for you:
RestfulRouter::make()->with('images')->resource(array('bundle' => 'articles'));
If you don't like how it sounds, you can pass one of these options istead of
images
: 'file', 'files', 'img', 'image', 'uploads'
- The last step is to configure a component for your form and, optionally, grid:
use Mobileka\L3\Engine\Form\Components\Image as ImageField,
Mobileka\L3\Engine\Grid\Components\Image as ImageColumn;
return array(
'form' => array(
'components' => array(
//...
'img' => ImageField::make('img'),
//...
)
),
grid' => array(
'components' => array(
//...
'img' => ImageColumn::make('img'),
//...
)
)
);
And... OH MY GOD! you did it! :) Now you can upload images and bind them to your Article objects in administration panel.
Please note that you can change file types that can be uploaded to a server in the
allowedFileTypes
parameter of theapplication/config/image.php
config file.
You just saw the Image
component usage example.
This component is there to upload images one by one (though you can use several Image components on the same page).
It is worth mentioning that you can add croppoing functionality to this component and that we use Jcrop
JavaScript library for this.
To enable image cropping you need to pass asceptRatio
as a Jcrop option like this:
Image::make('img')->jcrop(array('aspectRatio' => 1));
You can set other Jcrop options this way.
The next section describes how to retrieve these images and work with them.
Ok, it is time to show the uploaded image to a user. In order to get an original uploaded image you just need to call a getImageSrc()
method on a model object as follows:
$article = Article::find(1);
$article->getImageSrc('img'); // provide a name of an image field
But there are also other ways to get the image.
If you were following the installation quest carefully, you remember that it was required to create a application/config/image.php
configuration file.
Lets review the contents of this file:
<?php
return array(
'aliases' => array(
'multiupload_thumb' => array(99, 112), // Dimensions of thumbnails in multiupload
'admin_grid_thumb' => array(80, 80), // Dimensions of thumbnails in grid
),
'allowedFileTypes' => array('jpg', 'jpeg', 'png', 'gif') //modify for your needs
);
As you have already got from comments, in this file you can create aliases which reflect the image type (e.g. main_article_image) and provide dimensions for them.
This alias can be passed to the getImageSrc()
method as a second parameter: $article->getImageSrc('img', 'main_article_image');
This means that the original image will be rezised according to alias dimensions. By default, the aspect ratio will be preserved, but if you want to change this, add a third boolean item to the alias dimensions array like so:
return array(
'aliases' => array(
'main_article_image' => array(220, 170, false) //do not preserve the aspect ratio and make the image exactly 220 x 170
),
);
Please note, that the Engine will generate and save the image for an each alias when you access it the first time: on every other call the image will be read from a filesystem
It's easy to upload multiple images with the Engine. The only difference between single image uploading and multiuploading is a component that performs this. So, the Mobileka\L3\Engine\Form\Components\Image
component is used for a single image uploading and Mobileka\L3\Engine\Form\Components\MultiUpload
for multiple image uploading:
use Mobileka\L3\Engine\Form\Components\MultiUpload,
Mobileka\L3\Engine\Grid\Components\Image as ImageColumn;
return array(
'form' => array(
'components' => array(
//...
'img' => MultiUpload::make('img'),
//...
)
),
grid' => array(
'components' => array(
//...
'img' => ImageColumn::make('img'),
//...
)
)
);
When using a MultiUpload component, it is a common use case when you need to choose a main or, how it is called in Wordpress community, a featured image. If we take an example above, there are two steps to achieve this:
- Create
img
field in thearticles
table - Call
featuredImageSelector()
method on yourMultiUpload
component like this:
MultiUpload::make('img')->featuredImageSelector()
By default the featured image path will be saved in a field with a name of a component (img
in our case) and then can be accesses like this:
$article->img
You can change this passing a field name as a parameter to the featuredImageSelector()
method:
MultiUpload::make('img')->featuredImageSelector('featured_image')
Now you can access this image like $article->featured_image
(and don't forget to rename the field in the database table).
BTW not only images can be uploaded with this component. Change the
allowedFileTypes
parameter in theapplication/config/image.php
config to allow other file types to be uploaded.
Write me
Laravel engine includes CSRF protection for simple forms as well as for ajax calls.
In order to make this work, you need to perform these steps:
php artisan bundle:publish
- Add this JavaScript file to your layouts:
{{ HTML::script('bundles/engine/csrf.js') }}
- Add a metatag to your layouts in
<head>
section:{{ csrf_meta_tag() }}
- Add
engine_csrf
before filter for a route like this:Route::get('something', array('before' => 'engine_csrf', 'uses' => '...', 'as' => '...'));
If you are generating routes with RestfulRouter
class, every POST
, PUT
and DELETE
request is being protected automatically.
If you want to cancel this, call csrf()
method with parameter false
before resource()
method of the RestfulRouter
:
RestfulRouter::make()->csrf(false)->resource(array('bundle' => 'somebundle', 'module' => 'admin'));
Please note that CSRF protection is enabled for the administration panel by default
It's easy to configure the sidebar menu in administration interface.
it consists of items devided by sections. In other words, sections are groups or categories of menu items.
The first step is to create an application/config/menu.php
file and fill it with configuration data according to this syntax:
return array(
'sections' => array(
array(
'label' => 'Section Name 1',
'items' => array(
array(
'label' => 'Item Name 1',
'route' => 'item_route_alias', //according to routing conventions
'icon' => 'glyphicon-user'
),
//...
array(
'label' => 'Item Name 2',
'route' => 'bundle_admin_controller_action',
'icon' => 'glyphicon-group'
),
)
),
array(
'label' => 'Section Name 2',
'items' => array(
array(
'label' => 'User management',
'route' => 'users_admin_default_index',
'icon' => 'glyphicon-user'
),
)
),
)
);
There are two things we like about our menu:
-
Integration with user access control library which automatically checks whether the current authorized user has an access to a menu item and hides it when access is denied. When the user has no access to all section items, the section will be hidden too. To read about access control in detail, go to Access control section.
-
Cli generator generates menu items with proper routes and, if you pass additional information, it generates proper section and item names too. The only thing you need to configure manually is an icon associated with the menu item.
Read about generator in the next section.
Laravel Engine includes a script for a fast bundle generation. This is very useful if you need to get a simple (yet powerful and flexible) administration interface in no time.
There are two possible ways to generate an administration interface:
- One by one, specifying database fields for each bundle
- Mass bundle generation by reverse engineering of an existing SQL file
To create a new bundle run this command:
$ php artisan engine::create:bundle path.to.bundle.bundleName fieldName:laravelColumnType:option[ fieldName:laravelColumnType:option ...][ addmenu:section:item]
Here is a simple example:
$ php artisan engine::create:bundle app.Users username:string password:string role_id:unsigned:index
This generates following files with proper contents in bundles/app/Users
directory:
- config/default.php
- controllers/admin/default.php
- language/ru/default.php
- migrations/xxx_create_users_table.php
- migrations/xxx_add_users_foreign.php (with a foreign key for role_id)
- Models/User.php (with predefined belongs_to relation)
- routes.php
- start.php
Generator will also automatically add the new bundle to the application/bundles.php
file.
Finally, if addmenu
argument was not passed, generator will add the new bundle to a menu considering these defaults:
- The section will be called
app
- The menu item will be called
Users
In order to override these defaults, you can pass addmenu
argument as follows:
addmenu:SectionName:ItemName
To get more information about menu configuration, read the Admin sidebar configuration section.
As stated above, Laravel Engine is able to generate bundles by reading an existing SQL file. If you follow Laravel and Laravel Engine conventions while building an architecture of your database, you'll get a fully working administration interface as a gift.
To use this functionality, you need to put the sql file into the path('app')/schema
directory and run a command:
$ artisan engine::create:application[ schema_filename][ path_to_bundles]
The schema.sql
file will be expected by default. The second argument is for nesting your bundles in a separate directories inside of the bundnles
directory.
And here is an example:
Lets assume that you saved a file my_super_puper_database_schema.sql
in the path('app')/schema
directory and you also want all of your generated bundles to reside in bundles/app
directory. To do this, just run the following command:
$ artisan engine::create:application my_super_puper_database_schema.sql app
That's it! Now you have a fully functional administration interface with a grid (plus sorting and filtering possibilities) and CRUD with automatic form validation!
When generating bundles one by one, you have a lot of options to customize the generated code.
For example, you can make a field to be unsigned, create an index on it or make it to be required (a rule will be added to a self-validating model):
$ artisan engine::create:bundle app.Users username:string:required role_id:unsigned:index:required
Add following to you composer.json and run composer update
:
{
"require-dev": {
"phpspec/phpspec": "2.0.*@dev"
}
}
Now you can run tests by typing vendor/bin/phpspec run
.
To get more information about phpspec visit this Github page
See contributors
The Laravel Engine is open-source software licensed under the MIT License