SeinopSys
SeinopSys

Reputation: 8937

Instantiating ActiveRecord models without a database connection

I'm currently in the process of replacing a self-made hack-y MVC approach's Model component with php-activerecord. I have a separate page that displays in the event that the database server goes down, and that page used to display a user badge with a guest avatar, name and role. The code is as follows:

if (Auth::$signed_in)
    echo Auth::$user->getAvatarWrap();
else echo (new \App\Models\User([
    'name' => 'Guest',
    'role' => 'guest',
    'avatar_url' => GUEST_AVATAR
]))->getAvatarWrap();

Auth::$signed_in is set to true when a user is logged in, which - in case of a DB outage - is impossible, so the else branch executes with the predefined data.

Pre-ActiveRecord this would simply add the properties to the object and the call to the getAvatarWrap method would execute without any issues. Now, with the model being controlled by ActiveRecord, a set of additional calls take place for whatever reason. This could be because the model has relations defined, but I'm not sure.

ActiveRecord\DatabaseException: PDOException: SQLSTATE[08006] [7] could not connect to server: Connection refused
    Is the server running on host "localhost" (127.0.0.1) and accepting
    TCP/IP connections on port 5432? in /var/www/vendor/php-activerecord/php-activerecord/lib/Connection.php:260
Stack trace:
#0 /var/www/vendor/php-activerecord/php-activerecord/lib/Connection.php(260): PDO->__construct('pgsql:host=loca...', '<username>', '<password>', Array)
#1 /var/www/vendor/php-activerecord/php-activerecord/lib/Connection.php(122): ActiveRecord\Connection->__construct(Object(stdClass))
#2 /var/www/vendor/php-activerecord/php-activerecord/lib/ConnectionManager.php(33): ActiveRecord\Connection::instance('pgsql://databas...')
#3 /var/www/vendor/php-activerecord/php-activerecord/lib/Table.php(114): ActiveRecord\ConnectionManager::get_connection('pgsql')
#4 /var/www/vendor/php-activerecord/php-activerecord/lib/Table.php(90): ActiveRecord\Table->reestablish_connection(false)
#5 /var/www/vendor/php-activerecord/php-activerecord/lib/Table.php(71): ActiveRecord\Table->__construct('App\\Models\\User')
#6 /var/www/vendor/php-activerecord/php-activerecord/lib/Model.php(765): ActiveRecord\Table::load('App\\Models\\User')
#7 /var/www/vendor/php-activerecord/php-activerecord/lib/Model.php(271): ActiveRecord\Model::table()
#8 /var/www/includes/views/_sidebar.php(20): ActiveRecord\Model->__construct(Array)
#9 /var/www/includes/views/_layout.php(148): include('/var/www...')
#10 /var/www/includes/views/fatalerr.php(46): require('/var/www...')
#11 /var/www/includes/init.php(32): require('/var/www...')
#12 /var/www/includes/do.php(3): require('/var/www...')
#13 /var/www/public/index.php(1): require('/var/www...')
#14 {main} in /var/www/vendor/php-activerecord/php-activerecord/lib/Connection.php on line 262

How do I tell ActiveRecord to stop looking for a connection when I know for a fact that it won't find one, and that it should be content with the data I'm feeding it? Do I have to wave goodbye to using any of my models during an outage?

Upvotes: 2

Views: 320

Answers (1)

SeinopSys
SeinopSys

Reputation: 8937

Thanks to a comment I found out about SQLite memory databases which put me on the path to success. Given that the pull request to enable support for this kind of database has been collecting dust for almost a year I decided to take matters into my own hands.
If you're reading this later on, be sure to check whether said PR was merged yet, and leave a comment if it has so that I can update this answer.

First, I forked the original repository repository on GitHub. I'm intentionally not linking my fork, because it can get outdated or deleted eventually, so I suggest you make a fork for yourself. Then, I used a combination of this answer and this answer to get the magic branch into my fork from the fork of the PR's submitter. You can get the <url> by pressing the "Clone or download" button on the forked repository's home page.

$ git clone <url>
$ git remote add target https://github.com/claytonrcarter/php-activerecord.git
$ git fetch --all
$ git checkout master
$ git merge --squash target/sqlite-memory-support
$ git commit -m "Add support for SQLite :memory: databases"
$ git push

Now that my fork had the latest changes I took to my project's composer.json and changed the version of php-activerecord/php-activerecord in my require block to "dev-master", then added a repositories block with my fork in it. Again, replace <url> with the fork's clone URL.

{
    // ...
    "require": {
        // ...
        "php-activerecord/php-activerecord": "dev-master",
        // ...
    },
    "repositories": [
        {
            "type": "vcs",
            "url":  "<url>"
        }
    ]
}

But wait, the fun doesn't end there! Now that we have support for the memory SQLite DB adapter, we actually have to define it. So amend to your initialization config:

ActiveRecord\Config::initialize(function ($cfg){
    $cfg->set_connections([
        'pgsql' => 'pgsql://'.DB_USER.':'.DB_PASS.'@'.DB_HOST.'/database?charset=utf8',

        // Add this line below. 'failsafe' can be changed to something else, of course
        'failsafe' => 'sqlite://:memory:',
    ], 'pgsql');
});

Turns out it's pretty difficult to hot swap the default connection, so I did the next best thing, and made a child class that had its $connection set to the failsafe defined earlier. Note that the required fields have to be manually defined, because the lib has no means of knowing what fields to allow you to use from the empty database. Interestingly, the $has_many and similar relations that were present on the main class had no negative effect on the subclass' behavior, so I could keep it relatively short.

namespace App\Models;

/** @inheritdoc */
class FailsafeUser extends User {
    static $connection = 'failsafe';

    public $name, $role, $avatar_url;
}

Finally, I had to change the call to use the newly defined class:

if (Auth::$signed_in)
    echo Auth::$user->getAvatarWrap();
else echo (new \App\Models\FailsafeUser([
    'name' => 'Guest',
    'role' => 'guest',
    'avatar_url' => GUEST_AVATAR
]))->getAvatarWrap();

Low and behold, the script no longer breaks and I can finally get on with the ActiveRecord-ification of my codebase. If you'd like go through this torture yourself and have more than one model that you want to have an "offline" variant of then you might want to make an abstract class of some sort that automatically sets the properties you passed in the constructor along with pre-setting the $connection, making child instances ever so slightly less verbose.

Upvotes: 0

Related Questions