In this tutorial you will learn some advanced techniques in package development for BIIGLE, like creating new routes and views, and how to test them using the BIIGLE testing environment.
In a previous tutorial you have learned what PHP package development is all about and how to start developing your own BIIGLE module. If you haven't done the tutorial yet, start there and come back later, since we'll build upon that.
Now we would like to take our quotes
module and add a new route as well as a new view to the BIIGLE core application. Following the name of the package, the new view should display a random quote. We'll use the existing dashboard panel to add a link to the new view (otherwise the users will be unable to reach it).
But before writing any production code, there always comes the testing. If you never heard of Test Driven Development, go ask Uncle Bob and come back afterwards. Having a server application with restricted access and sensitive data thoroughly tested is always essential!
Testing our quotes
package on its own doesn't work for us, since we need Laravel for the routes, views and controllers we intend to implement. The core application has its testing environment already set up with all functional/unit tests residing in tests/php
. All you have to do is run composer test
in the root directory of the BIIGLE installation and the tests run. It would be best if we were able to test our package just like it would belong to the core application and fortunately there is a very easy way to do so.
As already mentioned in the previous tutorial, we are now able to develop the package right out of the cloned repository in vendor/biigle/quotes
. This is where we now create a new tests
directory besides the existing src
. Now all we have to do is to create a simple symlink from the tests/php
directory of the core application to the new tests
directory of our package:
ln -s ../../../vendor/biigle/quotes/tests/ tests/php/Modules/Quotes
Now the tests of our package are just like any other part of the core application and will be run with composer test
as well. Let's try testing a new test! Create a new test class in vendor/biigle/quotes/tests
called QuotesServiceProviderTest.php
with the following content:
<?php namespace Biigle\Tests\Modules\Quotes; use TestCase; use Biigle\Modules\Quotes\QuotesServiceProvider; class QuotesServiceProviderTest extends TestCase { public function testServiceProvider() { $this->assertTrue(class_exists(QuotesServiceProvider::class)); } }
You see, the test class is located in the Biigle\Tests
namespace and looks just like all the other test classes of the core application. You'll find lots of examples on testing there, too. For more information, see the Laravel and PHPUnit documentations. But does our test even pass? Check it by running PHPUnit in the root directory of the core application:
> composer testf QuotesServiceProviderTest PHPUnit 7.5.13 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 760 ms, Memory: 42.00 MB OK (1 test, 1 assertion)
Great! Now on to production code.
Usually, when creating a new view, we also need to create a new route. Routes are all possible URLs your web application can respond to; all RESTful API endpoints are routes, for example, and even the URL of this simple tutorial view is a route, too. What we would like to create is a quotes
route, like https://dias.senckenberg.de/quotes
. Additionally, only logged in users should be allowed to visit this route.
All routes of the core application are declared in the routes
directory. If you take a look at the files in this directory, you'll see that route definitions can get quite complex. Fortunately being able to add routes with custom packages is a great way of keeping things organized.
So just like the core application, we'll create a new src/Http
directory for our package and add an empty routes.php
file to it. For Laravel to load the routes declared in this file, we have to extend the boot
method of our QuotesServiceProvider
yet again:
<?php namespace Biigle\Modules\Quotes; use Biigle\Services\Modules; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; class QuotesServiceProvider extends ServiceProvider { /** * Bootstrap the application events. * * @param Modules $modules * @param Router $router * @return void */ public function boot(Modules $modules, Router $router) { $this->loadViewsFrom(__DIR__.'/resources/views', 'quotes'); $router->group([ 'namespace' => 'Biigle\Modules\Quotes\Http\Controllers', 'middleware' => 'web', ], function ($router) { require __DIR__.'/Http/routes.php'; }); $modules->register('quotes', ['viewMixins' => ['dashboardMain']]); } /** * Register the service provider. * * @return void */ public function register() { // } }
The new addition injects the $router
object into the boot
method. We then use this object to declare a new group of routes that will use the Biigle\Modules\Quotes\Http\Controllers
namespace and are defined in the src/Http/routes.php
file.
Now we can start implementing our first route.
But first come the tests! Since it is very handy to have the tests for routes, controllers and views in one place (those three always belong together), we'll create a new test class already called tests/Http/Controllers/QuotesControllerTest.php
looking like this:
<?php namespace Biigle\Tests\Modules\Quotes\Http\Controllers; use TestCase; class QuotesControllerTest extends TestCase { public function testRoute() { $this->get('quotes')->assertStatus(200); } }
Note how the directory structure always matches the namespace of the PHP class. With the single test function, we call the quotes
route we intend to implement, and check if the response is HTTP 200
. Let's check what PHPunit has to say about this:
> composer testf QuotesControllerTest PHPUnit 7.5.13 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 759 ms, Memory: 42.00 MB There was 1 failure: 1) Biigle\Tests\Modules\Quotes\Http\Controllers\QuotesControllerTest::testRoute Expected status code 200 but received 404. Failed asserting that false is true. /var/www/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:78 /var/www/vendor/biigle/quotes/tests/Http/Controllers/QuotesControllerTest.php:11
Of course the test fails with a 404
since we haven't implemented the route yet and it can't be found by Laravel. But that's the spirit of developing TDD-like! To create the route, populate the new routes.php
with the following content:
<?php $router->get('quotes', [ 'as' => 'quotes', 'uses' => 'QuotesController@index', ]);
Here we tell Laravel that the index
method of the QuotesController
class of our package is responsible to handle GET
requests to the https://dias.senckenberg.de/quotes
route. The router automatically looks for the class in the Biigle\Modules\Quotes\Http\Controllers
namespace, since we defined it that way in the service provider. We also give the route the name quotes
which will come in handy when we want to create links to it. Let's run the test again:
[...] 1) QuotesControllerTest::testRoute Expected status code 200 but received 500. Failed asserting that false is true. [...]
Now we get a 500
, that's an improvement, isn't it? You might have already guessed why we get the internal server error here: The controller for handling the request is still missing.
Controllers typically reside in the Http/Controllers
namespace of a Laravel application. We defined the src
directory of our package to be the root of the Biigle\Modules\Quotes
namespace so we now create the new src/Http/Controllers
directory to reflect the Biigle\Modules\Quotes\Http\Controllers
namespace of our new controller.
Let's create the controller by adding a new QuotesController.php
to the Controllers
directory, containing:
<?php namespace Biigle\Modules\Quotes\Http\Controllers; use Biigle\Http\Controllers\Views\Controller; class QuotesController extends Controller { /** * Shows the quotes page. * * @return \Illuminate\Http\Response */ public function index() { } }
The controller already extends the Controller
class of the BIIGLE core application instead of the default Laravel controller, which will come in handy in a next tutorial. Let's have a look at our test:
> composer testf QuotesControllerTest PHPUnit 7.5.13 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 762 ms, Memory: 42.00 MB OK (1 test, 1 assertion)
Neat! You can now call the quotes
route in your BIIGLE application whithout causing any errors. But wait, shouldn't the route have restricted access? If the user is not logged in, they should be redirected to the login page instead of seeing the quotes. Let's adjust our test:
<?php namespace Biigle\Tests\Modules\Quotes\Http\Controllers; use TestCase; use Biigle\Tests\UserTest; class QuotesControllerTest extends TestCase { public function testRoute() { $user = UserTest::create(); // Redirect to login page. $this->get('quotes')->assertStatus(302); $this->be($user); $this->get('quotes')->assertStatus(200); } }
We first create a new test user (the UserTest
class takes care of this), save them to the testing database and check if the route is only available if the user is authenticated. Now the test should fail again because the route is public:
[...] 1) QuotesControllerTest::testRoute Expected status code 302 but received 200. [...]
Restricting the route to authenticated users is really simple since BIIGLE has everything already implemented. User authentication in Laravel is done using middleware, methods that are run before or after each request and are able to intercept it when needed.
In BIIGLE, user authentication is checked by the auth
middleware. To add the auth
middleware to our route, we extend the route definition:
$router->get('quotes', [ 'middleware' => 'auth', 'as' => 'quotes', 'uses' => 'QuotesController@index', ]);
That was it. The auth
middleware takes care of checking for authentication and redirecting to the login page if needed. Run the test and see it pass to confirm this for yourself.
While the route and authentication works, there still is no content on our page. From the previous tutorial we already know how to implement a view, so let's create src/resources/views/index.blade.php
:
<blockquote> {{ Illuminate\Foundation\Inspiring::quote() }} </blockquote>
Now all we have to do is to tell the index
method of our QuotesController
to use this view as response of a request:
public function index() { return view('quotes::index'); }
Here, the quotes::
view namespace is used which we defined in the boot
method of our service provider in the previous tutorial. If we didn't use it, Laravel would look for the index
view of the core application. Now you can call the route and see the quote.
Pretty ugly, isn't it? The view doesn't look like the other BIIGLE views at all. It displays only the code we defined in the view template and nothing else. This is where view inheritance comes in.
The BIIGLE core application has an app
view template containing all the scaffolding of a HTML page and loading the default assets. This app
template is what makes all BIIGLE views look alike.
index.blade.php
view of our module:
@extends('app') @section('title', 'Inspiring quotes') @section('content') <div class="container"> <div class="col-sm-8 col-sm-offset-2 col-lg-6 col-lg-offset-3"> <blockquote> {{ Illuminate\Foundation\Inspiring::quote() }} </blockquote> </div> </div> @endsection
Here we tell the templating engine that our view should extend the app
view, inheriting all its content. The app
view has two sections we can use, title
and content
. The title
section is the content of the title tag in the HTML header. The content
section is the "body" of the BIIGLE view. Since styling the body of the page is entirely up to the child view, we have to use the Bootstrap grid to get adequate spacing.
Take a look at the page again. Now we are talking!
To finish up, we quickly add a link to the new route to the previously developed view mixin of the dashboard. Open the dashboardMain
view and edit the panel heading:
<div class="panel-heading"> <a href="{{ route('quotes') }}"><h3 class="panel-title">Inspiring Quote</h3></a> </div>
The route
helper function is an easy way to link to routes with a name. Even if the URL of the route changes, you don't have to change any code in the views.
That's it! Now you have learned how to create new routes, controllers and views, and how to test them. This is everything you need to develop complex custom modules where all the content is rendered by the server.
But there is still one step left for you to master package development: Custom assets. Besides using custom CSS to style the content beyond Bootstrap's capabilities, you need to be able to use custom JavaScript for interactive client side applications as well. In a next tutorial, we'll discuss how to include and publish custom assets and how to use the already provided functionality of the BIIGLE core client side application.