Using Laravel components individually

Starting from Laravel 4 all framework components are separate packages that can be used in any PHP project without using the laravel/framework package.

In this post, I'll demonstrate the usage of illuminate/container, illuminate/queue, illuminate/events, illuminate/redis and illuminate/database in a project without the Laravel framework.

Components

Container

Out of all components, illuminate/container is the easiest one to install and use.

<?php

use Illuminate\Container\Container;

// Get the global container instance
// https://laravel.com/api/8.x/Illuminate/Container/Container.html#method_getInstance
$container = Container::getInstance();

// Now, you can use the Container instance just like in Laravel!
return $container->make(Foo::class);

But we can go further and create the app() helper that we can use to quickly resolve dependencies.

Let's start by creating the file called helpers.php:

<?php

use Illuminate\Container\Container;

// https://github.com/laravel/framework/blob/8.x/src/Illuminate/Foundation/helpers.php#L105-L121
if (! function_exists('app')) {
    /**
     * Get the available container instance.
     *
     * @param  string|null  $abstract
     * @param  array  $parameters
     * @return mixed|\Illuminate\Contracts\Foundation\Application
     */
    function app($abstract = null, array $parameters = [])
    {
        if (is_null($abstract)) {
            return Container::getInstance();
        }

        return Container::getInstance()->make($abstract, $parameters);
    }
}

Don't forget to add helpers.php to the autoload section of the composer.json file:

"autoload": {
    "files": [
        "src/helpers.php"
    ],
    "psr-4": {
        "YourApp\\": "src/"
    }
}

Let's try it out.

<?php

// Same as Container::getInstance();
$container = app();

// Same as Container::getInstance()->make(Foo::class);
$foo = app(Foo::class);

Database

Basic setup and Query Builder

Conveniently, illuminate/database provides us with the Capsule\Manager class, that was designed for query builder integration outside of Laravel applications.

Let's start with the database connection configuration. We'll create the bootstrap.php file with the following contents:

<?php

// https://getcomposer.org/doc/01-basic-usage.md#autoloading
require_once __DIR__.'/../vendor/autoload.php';

use Illuminate\Database\Capsule\Manager;

$manager = new Manager();

// Similar to https://github.com/laravel/laravel/blob/8.x/config/database.php#L36
$manager->addConnection([
    'driver' => 'mysql',
    'host' => 'localhost',
    'database' => 'database',
    'username' => 'root',
    'password' => 'password',
    'charset' => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix' => '',
]);

// This allows us to use static methods for database connections
$manager->setAsGlobal();

We can try querying the database now:

<?php

use Illuminate\Database\Capsule\Manager;

// https://github.com/laravel/framework/blob/8.x/src/Illuminate/Database/Capsule/Manager.php#L191-L202
return Manager::table('orders')->get();

We can now send queries to the database using the query builder. But what about Eloquent ORM?

Eloquent

You can expect Eloquent ORM to work out of the box:

<?php

namespace YourApp\Models;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected $fillable = [
        'name',
    ];
}
<?php

use YourApp\Models\Order;

Order::first();

Migrations

We can use the schema method to obtain a Schema\Builder instance used to compose migrations:

<?php

use Illuminate\Database\Capsule\Manager;

Manager::schema()->create('orders', function ($table) {
    $table->bigIncrements('id');
    $table->string('name');
    $table->timestamps();
});

To run database migrations just execute the database migration file with php migration.php.

Events

Laravel already provides us with the dispatcher singleton, so we'll just register the service provider in the global container instance:

<?php

use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Events\EventServiceProvider;

(new EventServiceProvider(app()))->register();

// Don't forget to bind 'events' alias to Dispatcher contract
app()->bind(Dispatcher::class, 'events');

Redis

To use illuminate/redis you just need to bind the RedisManager instance to the redis alias:

<?php

use Illuminate\Redis\RedisManager;

app()->bind('redis', function () {
    // You can find information about available drivers here:
    // https://github.com/laravel/framework/blob/8.x/src/Illuminate/Redis/RedisManager.php#L157-L176
    return new RedisManager(app(), 'predis', [
        'default' => [
            'host' => '127.0.0.1',
            'password' => null,
            'port' => 6379,
            'database' => 0
        ]
    ]);
});

Queues

Similarly to illuminate/database, illuminate/queue also provides us with the Capsule\Manager class. However, we still need to implement the queue:work and queue:listen commands ourselves.

Let's start with the Application contract.

In Laravel, this contract provides us with:

Our implementation (without the actual implements Application syntax) of this contract will contain only one method - isDownForMaintenance, that will always return false, as our application won't need any maintenance (hopefully):

<?php

namespace YourApp;

use Illuminate\Container\Container;

class Application extends Container
{
    public function isDownForMaintenance()
    {
        return false;
    }
}

Example configuration for the database driver:

<?php

use Illuminate\Queue\Capsule\Manager;
use Illuminate\Queue\Connectors\DatabaseConnector;
use Illuminate\Database\Capsule\Manager as DatabaseManager;
use Illuminate\Database\ConnectionResolver;

$queue = new Manager(app());

// Add the database driver configuration
$queue->addConnection([
    'driver' => 'database',
    'table' => 'jobs',
    'connection' => 'default',
    'queue' => 'default'
]);

$queue->setAsGlobal();

// Create new connection resolver, with the default database connection under the hood
$resolver = new ConnectionResolver([
    'default' => DatabaseManager::connection()
]);

// Add database connector with resolver we created before
$queue->getQueueManager()
    ->addConnector('database', function () use ($resolver) {
        return new DatabaseConnector($resolver);
    });

Redis configuration example:

<?php

use Illuminate\Queue\Capsule\Manager;

$queue = new Manager(app());

$queue->addConnection([
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => 'default'
]);

$queue->setAsGlobal();

Now, we need to add the exception handler:

<?php

use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler;

class Handler implements ExceptionHandler
{
    public function report(Exception $e)
    {
        // Send something to Sentry, create an e-mail notification, etc.
    }

    // Request type is omitted, so 'illuminate/http' installation is not required
    public function render($request, Exception $e)
    {
        // Render error for the web context
    }

    public function renderForConsole($output, Exception $e)
    {
        // Render error for the console context
    }

    public function shouldReport(Exception $e)
    {
        // Define if an error should be reported
    }
}

app()->bind('exception.handler', function () {
    return new Handler();
});

Finally, let's implement the queue:work command:

<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\Capsule\Manager;
use Illuminate\Queue\WorkerOptions;

// Get the global queue manager instance
$queueManager = Manager::getQueueManager();

// Create new worker instance, with an event dispatcher and a custom exception handler
$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->daemon('default', 'default', new WorkerOptions());

To start a queue worker, use php worker.php.

For cases when your code has memory leaks requires testing you can use the queue:listen command.

Here, two PHP files are required, as queue job listener and executor are two different processes in this case.

listener.php:

<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Listener;
use Illuminate\Queue\ListenerOptions;

// In Laravel, by default, listener starts 'artisan' to execute new jobs,
// but in our case 'process.php' will take that role
// https://github.com/laravel/framework/blob/8.x/src/Illuminate/Queue/Listener.php#L72-L75
define('ARTISAN_BINARY', 'process.php');

$worker = app(Listener::class, [
    // Both files are in the same folder, so we'll stick to __DIR__
    'commandPath' => __DIR__
]);

// All 'process.php' output will be piped here
$worker->setOutputHandler(function ($type, $line) {
    echo $line;
});

$worker->listen('default', 'default', new ListenerOptions());

process.php:

<?php

require_once __DIR__.'/bootstrap/bootstrap.php';

use Illuminate\Queue\Worker;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Queue\Capsule\Manager;

// Same as with 'queue:work', but instead of running a daemon process
// we'll just wait for one job, execute it, and pass control back to the 'listener.php' file
$queueWorker = Manager::getQueueWorker();

$worker = new Worker($queueManager, app('events'), app('exception.handler'));

$worker->runNextJob('default', 'default', new WorkerOptions());

You can start listening with php listener.php.

Queueing new jobs looks like this:

<?php

use Illuminate\Queue\Capsule\Manager;

Manager::push(SomeJob::class);