Advanced module development

In this tutorial you will learn some advanced techniques in module 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 module, 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

Testing our quotes module 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 module 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 module 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 module:

ln -s ../../../vendor/biigle/quotes/tests/ tests/php/Modules/Quotes

Now the tests of our module 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.

A new route

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 {{ url('quotes') }}. Additionally, only logged in users should be allowed to visit this route.

Adding routes with a custom module

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 modules is a great way of keeping things organized.

So just like the core application, we'll create a new src/Http directory for our module 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.

Implementing a new 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 module is responsible to handle GET requests to the {{ url('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.

A new controller

Controllers typically reside in the Http/Controllers namespace of a Laravel application. We defined the src directory of our module 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.

Creating a 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 without 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.
[...]

Middleware

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.

A new view

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.

Inheriting views

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. The Blade templating engine allows for view inheritance so you can create new views, building upon existing ones. When inheriting a view, you need to specify view sections, defining which part of the new view should be inserted into which part of the parent view. Let's see this in action by applying it to the 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.

Conclusion

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 module 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.