I will introduce the Laravel package: Live Statics
Let's start at the end and watch the package in action before digging any deeper.
The actual example is this site itself! Let's add the subdomain `static` to the URL to enable live-statics. What's going to happen is that the site will be injected with our mocked classes instead of the real ones, providing us with fake content that will emulate how the real site will behave.
Please go ahead and click around:
This is not a different codebase! We will be providing our application with a mechanism to just switch between real or fake data sources. This the basic concept for this package, and what this article will introduce.
The point is to avoid creating database tables, services, or basically anything besides a few mocked classes before actually modelling our real classes. This process makes prototyping much faster.
Once we are satisfied, and after our views are in place, integrating real data sources involve almost no work at all! The interfaces are the same so our views will require no change. On this case I have simply used Eloquent.
Problems to solve
1. Create a switch between real or fake data.
2. Create a mocked object good enough to emulate real behaviors and data structure used by our system.
Our views won't have any awareness if our data is coming from a real or a fake source, as they will access objects that respond to the same interface. This will give us an amazing consequence, Views will be real! no more static deliverables.
Integration efforts down the road will be virtually zero.
As we previously saw, we will be using the presence or absence of the `static` subdomain as a switch.
To implement this solution, we will be using Dependency Injection through interfaces. If you are not familiar with this concept, please give it a read and come back.
Take a look to the following Laravel Controller:
<?php
namespace App\Http\Controllers;
use \App\Interfaces\Models\ThemeInterface;
class ThemesController extends Controller
{
public function index(ThemeInterface $model)
{
$collection = $model->all();
return view('themes', [ 'items' => $collection ]);
}
}
What we are trying to do is to load all "Themes" and pass them to the view as a collection.
Observe line 8 in particular, a `ThemeInterface` binded object is being injected.
This is key, we can control which object we are binding to an interface, so by adding a switch, the same interface could represent different entities!
Ok, now let's consider the following code:
$this->app->bind(\App\Interfaces\Models\ThemeInterface::class, function () use ($enabled) {
if ($enabled) {
return \App\Mocks\Models\ThemeMock::create();
} else {
return app(\App\Models\Theme::class);
}
});
We are passing a boolean `$enabled` to the `bind` function and using it as a switch. This way we will be able to bind this interface to our Eloquent model object, or our mocked object dynamically.
We saw on previous examples that this switch is turned on using the `static` subdomain.
Let's study the simplified version for now:
$domain = request()->server->get('HTTP_HOST');
$subdomain = explode('.', $domain)[0];
$enabled = $subdomain == 'static';
This way `$enabled` will be true if `static` is our subdomain, and false otherwise.
The Mocked Eloquent object
After trying many prototypes, the most general approach was to simply create a class that emulates Eloquent's interface. To achieve this we will use normal classes and a testing suite under the hood to manage our functions, expectations and attributes.
The package we will use for this purpose is Mockery. Please give a quick glance to the docs. We will come back to it a the end of this article explaining a few advantages of using it.
Using a test suite to build mocks will give us for free a very simple way to define attributes, functions and all values it should return, partial mocks, and more.
Take a look at the base class `BaseMock`, in particular at the static function `create` that will return a mocked object:
public static function create()
{
// Create a mocked object
// Base class (if present) will implement Mockery partial mocks (see documentation)
if (static::$baseClass) {
$mock = \Mockery::mock(static::$baseClass, static::$baseInterface)->makePartial();
} else {
$mock = \Mockery::mock(static::$baseInterface);
}
// Define base methods and attributes for the mocked model
static::define($mock);
return new static($mock);
}
// To be redefined in child classes
public static function define(&$mock)
{
return $mock;
}
The only extra work that we have to do in our inherited models then is to overload `define`:
namespace App\Mocks\Models;
use \Petrelli\LiveStatics\BaseMock;
use \App\Interfaces\Models\ThemeInterface;
use Illuminate\Support\Str;
class ThemeMock extends BaseMock implements ThemeInterface
{
public static $baseInterface = ThemeInterface::class;
public static function define(&$mock)
{
$mock->title = app('faker')->words(rand(1, 3), true);
$mock->slug = Str::slug($mock->title);
$mock->description = app('faker')->sentence(50);
return $mock;
}
}
We defined 3 parameters: title, slug and description. Values are filled up using `Faker`.
Remember that `$mock` is a `Mockery` object, so we can implement any of it's underlying functionalities! For example, let's create a function that returns different values depending on the received parameters, and some extra functions using Mockery's `allows`
public static function define(&$mock)
{
// ...
// define the image() function, if it's called with the argument 'hero', return a big placeholder
$mock->shouldReceive('image')
->withArgs(['hero'])
->andReturn('http://placehold.it/1500x500');
// define the image() function, if it's called with the argument 'profile', return a small square placeholder
$mock->shouldReceive('image')
->withArgs(['profile'])
->andReturn('http://placehold.it/250x250');
// define functions 'tickets', and 'find' with their return values regardless of the arguments
$mock->allows([
'tickets' => [1,2,3],
'get' => null,
]);
// ...
return $mock;
}
`BaseMock` will work as the following: if those attributes or methods are not present on the inherited class itself, it will get them from the underlying Mockery object.
Now our first example is finally starting to make sense. If `static` subdomain is present, `$enabled` will be true, and therefore, the provider will bind a `ThemeMock` object, otherwise, a real `Theme` Eloquent object will be linked.
So now, every time we inject `ThemeInterface` into our controllers (or any function), we will get the object depending entirely on the subdomain! The code does not vary from there to integration
Advanced features
Real URL's
Take a quick look to the `BaseMock` class definition, you will notice that implements the interface `UrlRoutable`:
class BaseMock implements Arrayable, UrlRoutable
This basically means that it will allow us to generate real URL's when passing them to the Laravel route helpers!
{!! route('themes.show', $item) !!}
It doesn't matter if the object is real or fake, the URL will be generated correctly.
This means, you will be able to navigate clicking around a completely mocked site! given that it's under a subdomain the generated URL's will correctly keep you within the live-statics boundaries.
This library will also allow the following advanced features:
* Pagination emulation for collections
* Partial mocking. Basically you can pass an existent class and redefine only the functions you want to mock. Useful when mocking services. Courtesy of Mockery.
* Versioning. You could have different versions of mocked classes to represent states of your application. This can be done by passing an URL parameter, or by subdomain (static2, static3, etc).
* Blade directives. Hide or show stuff by using @static @endstatic @nonstatic @endnonstatic
Conclusion
For many of my projects and personal sites, the overall speed to prototype new functionalities increased, and also allowed us to share progress immediately with clients. Because shared links were actually real, delivering a *live-static* site to play around has been a game changer.
The concept is very simple, and so the codebase and execution. Please give it a try and of course any idea is welcome!
F.