Over the last year, I took part in three large projects. My task was to move away from the old architecture based on PHP and server-side HTML generation, and transition to REST API.
With the old approach, back-end developers were expected to know much more about the UI and visual aspects of the application. Because of this, they had to pay attention to different segments of the application, instead of focusing on their primary objective. Having the back-end API strictly separated from the UI allowed our developers to focus on the quality of their code.
Also, testing API services is much easier as REST API can be verified by automated unit testing.
I’ve had some experience in writing my own framework, as well as working with Yii, CakePHP, CodeIgniter, Slim Framework, Symfony and few other open source frameworks. Each time, I’ve experienced a lack of functionality or awkward approach to some problems.
I used Laravel for four months before deciding to choose it as the platform for our next project. The project itself was a great success and this article is a product of this experience. Now I am able to call myself a Laravel developer.
Why I Chose Laravel
I’ve already outlined some of my reasons for using Laravel and my experience, so let’s take a closer look at what made Laravel a better choice for my latest project:
- Quick and functional core that can be extended
- Clean and simple routing
- Effective ORM and database layer
- Easy integration with third-party libraries (AWS, export libs, etc.). You can use Composer or Packagist to include libraries in your project
- Active and growing community that can provide quick support and answers
- Supporting unit tests out of the box
- Async queue and background jobs for the long running tasks
Laravel Core And Routing
The Laravel kernel is hosted on GitHub. The kernel implements an IoC pattern allowing customization and rewriting of any part of the framework (request, logging, authentication, etc.).
Laravel designers didn’t spend too much time reinventing the wheel. A lot of solutions and practices are transferred from other frameworks. A good example of this approach is the extended Symfony console called Artisan, which is a command-line interface included with Laravel.
Routing
Routing in Laravel is amazing; it is very similar to the Ruby on Rails (RoR) implementation, which I like a lot. You can easily group routes, create resources for CRUD pages, attach filters and automatically bind models to the request parameters.
Nested routes are a very useful feature:
Route::group(['prefix'=>'level0'], function(){
Route::get('/', array('uses' => 'TestController@level0'));
Route::group(['prefix'=>'/{level0}/level1'], function(){
Route::get('/', array('uses' => 'TestController@level1'));
Route::post('/{custom_variable}/custom_route', array('uses' => 'CustomConroller@custom_route'));
});
});
Versioning can be implemented as a group on the top level for all nested routes by using the ‘v1’ prefix. When we change the API version, we can retain the old one and use the ‘v2’ prefix to start implementation of routes with new code and logic, i.e., new references to controllers and actions.
Let’s take a step-by-step look at everything used in this Laravel tutorial:
A defined group of routes with path level0
at top level of our API.
If we have a call BASEURL/level0
, then Laravel will resolve it and call method level0()
of TestController to process the request.
We have a sub-group with the {level0}/level1
pattern. To access API resources inside this group we should use the path for the parent group (level0)
and match the pattern of our sub-group ({level0}/level1)
. For example, level0/777/level1
is the correct path for this sub-group of our API. Here we have 777 as the variable level0 that Laravel will path to the handler of the routes inside the sub-group.
In the the end we have two sample routes:
BASEURL/level0/777/level1
– GET request to this URI will be processed with method level1($level0)
of TestController where the first parameter is {level0}
and it will be initialized with value 777.
BASEURL/level0/777/level1/888/custom_variable
– POST request to this URI will be processed using the custom_route($level0, $custom_variable)
method of CustomController.
The parameters used in the method come from the route variables.
Finally, we can associate our variable {level0}
in the route with a model, i.e., LevelModel. In this case, the framework automatically tries to find existing database records and pass it on as a parameter to the controller’s method.
It helps us write less code and we don’t need to write LevelModel::find($id)
or LevelModel::findOrFail($id)
in controllers.
The Dingo API package takes routing one step further.
Here are a few features that Dingo provides to the framework:
- Transformers: Special objects for response customization (e.g., type-cast integers and boolean values, paginate results, include relationships, etc.)
- Protect routes and allow different/custom authentication providers. Controller Trait provides protection of custom actions, while we can easily retrieve authenticated user info with
API::user()
. - Limiting request count per user by using built-in or custom throttles.
- A powerful internal requests mechanism that allows requests on your API internally
Laravel uses Eloquent ORM
Laravel is based on Eloquent ORM. I’ve used it with PostgreSQL and MySQL, and in both cases it performed flawlessly.
The official documentation is comprehensive, so there is no reason to repeat things in this article:
Query Scope – query logic is created as a function with a special scope prefix.
The following example shows the standard SQL Query transformed to a query function in Eloquent ORM:
SELECT * WHERE hub_id = 100 AND (name LIKE `%searchkey%` OR surname LIKE `%searchkey%`):
function scopeByFieldListLike($query, $fields, $value){
$query->where(function($query) use ($fields, $value){
foreach($fields as $field){
$query->orWhere($field , 'like', "%".$value."%");
}
});
return $query;
}
Using a function in your code is straightforward.
$model = new User;
$model->byFieldListLike(['name', 'surname'], 'searchkey');
A lot Eloquent methods return an instance of the QueryBuilder. You can use almost all of these methods on models.
Unit Testing
Writing unit tests is usually very time consuming, however, it’s definitely worth the time, so please do it.
Laravel relies on a TestCase base class for this task. It creates a new instance of the application, resolves the route and runs the controller method in its own sandbox. However, it does not run application filters (App::before and App::after) or more complex routing scenarios. To enable these filters in a sandbox environment, I had to manually enable them by using Route::enableFilters()
.
Obviously, if you know how to use Laravel, you know it could use some more work in the unit testing segment. I’ve set up a few functions that helped me create more advanced unit tests. Feel free to take these and use them in your projects.
To perform more advanced real-life testing and implement “curl-like” requests, I used the kriswallsmith/buzz library. This provided me with a much needed feature set for testing, including custom headers and uploading files.
The code below shows the function you can use to execute this kind of test:
public function browserRequest($method, $resource, $data = [], $headers = [], $files = [])
{
$host = $this->baseUrl();
if (count($files)){
// Use another form request for handling files uploading
$this->_request = new Buzz\Message\Form\FormRequest($method, $resource, $host);
if (isset($headers['Content-Type'])) {
// we don't need application/json, it should form/multipart-data
unset($headers['Content-Type']);
}
$this->_request->setHeaders($headers);
$this->_request->setFields($data);
foreach($files as $file) {
$upload = new Buzz\Message\Form\FormUpload($file['path']);
$upload->setName($file['name']);
$this->_request->setField($file['name'], $upload);
}
$response = new Buzz\Message\Response();
$client = new Buzz\Client\FileGetContents();
$client->setTimeout(60);//Set longer timout, default is 5
$client->send($this->_request, $response);
} else {
$this->_request = new Buzz\Message\Request($method, $resource, $host);
$this->_request->setContent(json_encode($data));
$this->_request->setHeaders($headers);
$response = new Buzz\Message\Response();
$client = new Buzz\Client\FileGetContents();
$client->setTimeout(60);//Set longer timout, default is 5
$client->send($this->_request, $response);
}
return $response;
}
I am a very lazy developer, so I added optional parameters that allowed me to decode responses, extract data, and check the response codes. This led to another method that includes default headers, authorization tokens and some additional functionality.
public function request($method, $path, $data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
{
if (!is_array($data)){
$data = array();
}
$servers = ['Content-Type' => 'application/json'];
if ($this->_token){
$servers['authorization'] = 'Token ' . $this->_token;
}
$this->_response = $this->browserRequest($method, $path, $data, $servers, $files);
if ($autoCheckStatus === true){
$this->assertTrue($this->_response->isOk());
} elseif(ctype_alnum($autoCheckStatus)){
$this->assertEquals($autoCheckStatus, $this->_response->getStatusCode());
}
if ($autoDecode){
$dataObject = json_decode($this->_response->getContent());
if (is_object($dataObject))
{
// we have object at response
return $this->_dataKeyAtResponse && property_exists($dataObject, $this->_dataKeyAtResponse) ? $dataObject->{$this->_dataKeyAtResponse} : $dataObject;
}
elseif (is_array($dataObject))
{
// perhaps we have a collection
return $this->_dataKeyAtResponse && array_key_exists($this->_dataKeyAtResponse, $dataObject) ? $dataObject[$this->_dataKeyAtResponse] : $dataObject;
}
else
{
// Uknown result
return $dataObject;
}
} else {
return $this->_response->getContent();
}
}
Once again, my lazy habits made me add wrappers designed to test CRUD pages:
create($data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
show($id, $data = [], $autoCheckStatus = true, $autoDecode = true)
update($id, $data = [], $autoCheckStatus = true, $autoDecode = true, $files = [])
delete($id, $data = [], $autoCheckStatus = 204, $autoDecode = false)
index($data = [], $autoCheckStatus = true, $autoDecode = true)
A simple test code would look like this:
// send wrong request
// Validation error has code 422 in out specs
$response = $this->create(['title'=>'', 'intro'=>'Ha-ha-ha. We have validators'], 422);
// Try to create correct webshop
$dataObject = $this->create(
$data = [
'title'=>'Super webshop',
'intro'=>'I am webshop',
]
);
$this->assertGreaterThan(0, $dataObject->id);
$this->assertData($data, $dataObject);
// Try to request existing resource with method GET
$checkData = $this->show($dataObject->id);
// assertData is also a help method for validation objects with nested structure (not a part of PHPUnit).
$this->assertData($data, $checkData);
// Try to update with not valid data
$this->update($dataObject->id, ['title'=> $data['title'] = ''], 422);
// Try to update only title. Description should have old value. Title - changed
$this->update($dataObject->id, ['title'=> $data['title'] = 'Super-Super SHOP!!!']);
// Then check result with reading resource
$checkData = $this->show($dataObject->id);
$this->assertData($data, $checkData);
// Read all created resources
$list = $this->index();
$this->assertCount(1, $list);
// TODO:: add checking for each item in the collection
// Delete resoure
$this->delete($dataObject->id);
$this->show($dataObject->id, [], 404);
Here is another useful function for validating response data including relations and nested data:
public function assertData($input, $checkData){
// it could be stdClass object after decoding json response
$checkData = (array)$checkData;
foreach($input as $k=>$v){
if (is_array($v) && (is_object($checkData[$k]) || is_array($checkData[$k]))) {
// checking nested data recursively only if it exists in both: response data($input) and expected($checkData)
$this->assertData($v, $checkData[$k]);
} else {
$this->assertEquals($v, $checkData[$k]);
}
}
}
Queuing
Long running tasks are a common bottleneck in web applications. A simple example would be the creation of a PDF report and its distribution inside an organisation, which can take a lot of time.
Blocking user requests to perform this action is not a desirable way of addressing the issue. Using Queue
in Laravel is a great way of handling and queuing long running tasks in the background.
One of the last issues was PDF generation and sending reports via email. I used a default queue driver for Beanstalkd, supported with the package pda/pheanstalk. It is very easy to add tasks to the queue
For example, generating and sending PDFs could be done like this:
\Queue::push('\FlexiCall\Queue\PdfSend', [....data for sending to job's handler..], 'default');
Our PdfSend
handler could be implemented like this:
class PdfSend extends BaseQueue{
function fire($job, $data){
//.............................
//..... OUR IMPLEMENTATION.....
//.............................
}
}
Laravel specifications suggest tracking a queue with --daemon
options, but I use the supervisor daemon that keeps it running permanently:
php artisan queue:listen --env=YOUR_ENV
Queues are great for tasks like saving data. They can be used to repeat failed jobs, add sleep timeouts before executing jobs, etc.
Putting It All Together
I’ve mentioned few key points related to Laravel development in this Laravel review. How you put it all together and build your application will depend on your specific setup and project. However, here is a brief checklist of steps you might want to consider:
- Setup homestead Vagrant box prepared for development using Laravel and configured local environment.
- Add endpoints (configure routes).
- Add authentication layer.
- Add filters before/after application execution, giving you an option to run anything before and after application is executed. Additionally, you can define filters for any route.
My default “before” filter creates a global scope as a singleton and starts the timer for monitoring performance:
class AppBefore {
function filter($request) {
\App::singleton('scope', function() {
$scope = new AppScope();//My scope implementation
$scope->startTimer();
return $scope;
});
}
}
The “after” filter stops the timer, logs it to the performance log, and sends CORS headers.
class AppAfter{
function filter($request, $response){
$response->header('Access-Control-Allow-Origin', '*');
$response->header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
$response->header('Access-Control-Allow-Headers', 'Content-Type');
$response->header('Access-Control-Max-Age', '86400');
\App::make('scope')->endTimer();
\App::make('scope')->logTotalTime();
return $response;
}
}
Developing each of your modules will likely require these steps:
Setup data migration, and seed default data.
Implement models and relations, transformers, validators and sanitizers (if needed).
Write unit tests.
Implement controllers and execute tests.
Wrap Up
I’ve been using Laravel for over a year. As with any technology, there were some teething problems, but I believe that learning Laravel has convinved me that it is an amazing framework. There are a few more things that I haven’t mentioned in this Laravel tutorial, but if you are interested in learning more, you may want to consider these points and features:
- Powerful model for subscribing and listening to application events.
- Support for Amazon SDK aws/aws-sdk-php.
- Caching.
- Own templating engine called Blade, if you like building the application “the old way” without RESTful backend and UI separation.
- Simple configuration.
- Built-in Stripe billing module.
- Out of the box localization.
- Catching and handling most errors.
Good luck and stay tuned, Laravel is a promising framework and I believe it will be around for years to come.
This post is originally published in Toptal’s blog written by Alex Rytov. He is freelance software engineer from Toptal.