Evan Hildreth
About Archive Now Also on Micro.blog
  • So I thought the Toy Story Funday Football was gonna be a fun gimmick, but…

    It’s a tech demo. And not a good one at that. There’s some fun ideas here, but the real-time animation is just Not There Yet. Unknown players, bad ball spotting, wonky camera; like watching a Let’s Play of Sonic 2006.

    → 9:44 AM, Oct 1
  • That’s How We’re Gonna Win: Not Fighting What We Hate, Saving What We Love.

    With that in mind, what do we love about social media?

    → 8:42 PM, Sep 28
  • Follow-up to a previous question: the WGA tightened their rules for talk shows after 2008:

    …“writing” could mean anything from scripting a monologue to developing and researching questions for guests or putting those words on cue cards.

    (via @KevvyC062@mastodon.social)

    → 9:06 AM, Sep 17
  • Trying to avoid having a “quick” manual process instead of building another setting panel or extra admin screen.

    Another couple of hours now saves me:

    • The 5 min process
    • The 30 min context switch
    • The 10 min of support communication
    • Et Cetera

    Every. Time.

    → 12:17 PM, Sep 16
  • TiPhone. To go with the TiBook I never had.

    → 1:56 PM, Sep 12
  • Genuine question: how is Drew Barrymore starting her show back without writers different from Conan, Colbert, and Stewart starting theirs back in 2008? I don’t remember them getting in trouble for strike breaking.

    → 7:02 PM, Sep 11
  • Hot take: “Don’t Fear the Reaper” actually does need more cowbell.

    → 8:39 AM, Sep 11
  • Bookmarking for later: Creating a card game in Tabletop Simulator.

    → 5:11 PM, Sep 10
  • Been a fan of Cory Godbey for years and SUPER excited to see his work in the TCG world!

    Picture of the “Magic Golden Flower” card from Disney’s Lorcana trading card game
    → 6:04 PM, Sep 9
  • Based on Stratechery’s summary of the Disney-Spectrum dispute du jour, I’m actually pulling for Spectrum? To wit:

    D: Pay us more.
    S: Give us Disney+ where the good stuff is.
    D: lol no
    S: 😎
    D: But we’ll turn off ESPN!
    S: We’ll send them to YouTube ourselves.
    D: But…
    S: 😎

    → 7:19 PM, Sep 5
  • A Stupidly Simple PHP Dependency Injection Container

    I’ve already written at length about dependency injection. And in the months since it’s only proven to be more helpful. But just because I got over some of my hangups about SOLID doesn’t mean I got rid of all of my bad habits.

    Particularly the bad habit of deciding that even though there’s a perfectly servicable library I’m already using, I can’t ignore the persistent thought that I can do better.

    So I did. I got irked by something and ended up writing my own dependency injection container.

    The lay of the land

    The way I’m building Smolblog’s core library at the moment separates objects into two categories:

    • Value objects contain strongly-typed information. They are (mostly) read-only, as mutation should only happen in certain places. Any methods in a Value object should be self-contained; they do not call out to other objects, Services, or dependencies. Value objects are state, not code.
    • Service objects perform actions. They can have dependencies on other services and objects and should be given those dependencies at construction. They can act on information in Value objects; those should be given to the Service when the particular method is called. Services should typically not contain data; they should be able to function as singletons (regardless of whether they actually are). Service objects are code, not state.

    Having this separation has actually really helped me focus the architecture in Smolblog, and it’s kept me from making any one class too “big.”

    Dependency injection containers are classes that store dependencies for other classes and can provide instances of them. Essentially, instead of creating new instances, you get new instances from the container.

    Where normal code might create a service like this:

    $service = new Service(db: new Database(), fetch: new HttpClient());
    

    Using a dependency injection container, it would look like this:

    $service = $container->get(Service::class);
    

    This takes all the responsibility for knowing how to instantiate a service away from the classes throughout the application and centralizes it into one place.

    Containers are a common pattern, such that there is a common interface for containers to use: PSR-11. This way, different frameworks and libraries can define containers, and other libraries can use them without having to depend on the specific behavior of specific containers. For a while, I was using Container from The League of Extraordinary Packages as the container for Smolblog.

    Until I wasn’t.

    Preoccupied with whether I could

    Let me state two things first:

    1. I could not have built my own container at the outset. I needed to fully grasp the concept first, and that could only happen by writing my own code against another library.
    2. Nothing in this article is a dig against the League’s Container. I want to be absolutely clear on this. I’m not interested in starting drama or picking fights.

    But as I got more and more into using dependency injection, especially in the very specific ways I was using it for Smolblog, I realized how… simple the concept was.

    In PHP, every class has a static constant class that is simply a string of the fully-qualified class name:

    namespace oddEvan\Example\Simple;
    
    class StupidSimple {}
    
    echo StupidSimple::class;
    // oddEvan\Example\Simple\StupidSimple
    

    Passing that string into a container’s get method will typically return an instance of that class.

    Now let’s consider the constraints I have for Smolblog:

    1. The only classes with dependencies are Services.
    2. Services should be given all dependencies at construction.
    3. Services should function as singletons.

    This makes our container’s logic… actually pretty simple:

    1. Have a configuration of classes and dependencies.
    2. Given a class name, check for an existing instance and skip to step 6 if there is one.
    3. If no instances, retrieve that class' dependencies.
    4. For each class in those dependencies, call step 2 with the dependency’s class.
    5. Use the dependencies to create an instance of the class and store it.
    6. Return the instance to the caller.

    …I think we can do this.

    Considered whether I should

    That’s cool and all, but replacing an established library with my own implementation is not something to be done lightly. A well-built library, like the ones from the League, are well-tested and well-maintained by a group of people. I’m just me.

    By rolling my own solution, I’m eschewing the time and effort put into the existing library. Sometimes it can look like “bloat” or “unnecessary” code, but often that code covers edge cases that aren’t immediately obvious. Some of those potential bugs can even be security concerns.

    In this specific case, a lot of the code in the League’s container involves different ways to load classes into the container. Because it is a general-purpose library, it has to handle several different scenarios:

    • Singleton classes (return the same instance every time)
    • Multiple classes (return a new instance every time)
    • Set dependencies in the constructor
    • Set dependencies by method calls after construction
    • Store classes by name
    • Store classes by aliases
    • Receive an initial configuration
    • Accept changes at any time
    • Determine if a dependency is another class or a value

    With Smolblog’s constraints, this list is a lot shorter:

    • Singleton classes
    • Set dependencies in the constructor
    • Store classes by name
    • Receive an initial configuration
    • Determine if a dependency is another class or a value
    • Uses named arguments

    That last point is what tipped me over to writing my own container. In PHP 8, you can now use named arguments. This is a language construct I first saw in Objective-C that Apple carried over into Swift, and understandably so. It makes method calls much more readable, especially if they have many optional parameters. Let’s start with an obtuse function:

    make_superhero('Larry', 'Larry-Boy', 'Archibald', 3, false);
    

    With named arguments, not only is it clearer what argument is what, but the order is no longer significant:

    make_superhero(
      super_name: 'Larry-Boy',
      num_episodes: 3,
    	citizen_name: 'Larry',
      assistant: 'Archibald',
      can_super_size: false,
    );
    

    I’ve been using named arguments extensively in Smolblog, and I wanted that flexibility in my container. And wanting that feature is ultimately what let me give myself permission to write my own container. It wasn’t—and isn’t!—enough just to want “less code”; there has to be a reason for me to write my code.

    So let’s get to it.

    Level 1: it begins

    We’ll start with a naive implementation just to get an idea of where we are, a simple configuration and handler.

    Let’s set up some pretend services first:

    class DatabaseService {
      public function __construct() {
        $this->connection = new DatabaseConnection('db://user:pass@server/db');
      }
      //...
    }
    
    class UserService {
      public function __construct(private DatabaseService $db) {}
      //...
    }
    
    class UserApiService {
      public function __construct(private UserService $users) {}
      //...
    }
    

    For configuration, we’ll create an array of arrays. Each array will contain a class' dependencies, and we’ll key that array to the class' name:

    $config = [
      UserApiService::class => [
        'users' => UserService::class,
      ],
      UserService::class => [
        'db' => DatabaseService::class,
      ],
      DatabaseService::class => [],
    ];
    

    And now, our container:

    class Container implements Psr\Container\ContainerInterface {
      private array $instances = [];
      
      public function __construct(private array $config) {}
    
      public function has(string $id): bool {
        return array_key_exists($id, $this->config);
      }
      
      public function get(string $id) {
        // Check if $id is in the configuration.
        if (!$this->has($id)) { throw new ServiceNotFoundException($id); }
        
        // If we don't already have an instance, create one.
        $this->instances[$id] ??= $this->instantiateService($id);
        
        // Return the instance.
        return $this->instances[$id];
      }
      
      private function instantiateService(string $id) {
        // Get the listed dependencies from the container.
    		$args = array_map(
    			fn($dependency) => $this->get($dependency),
    			$this->config[$id]
    		);
    
    		return new $service(...$args);
      }
    }
    

    Simple! But these are hardly real-world conditions.

    Level 2: Other Parameters

    Now let’s say we want to make DatabaseService more resilient. Instead of having a hard-coded database connection string, we’ll pass one into the constructor:

    class DatabaseService {
      public function __construct(string $connectionString) {
        $this->connection = new DatabaseConnection($connectionString);
      }
      //...
    }
    

    Now we just add that string to our configuration… wait…

    $config = [
      //...
      DatabaseService::class => [
        'connectionString' => 'db://user:pass@server/db', // This is ambiguous
      ]
    ];
    

    Remember that the class constants are just strings. How is our container going to tell the difference between a class string like oddEvan\Thing\DatabaseService and db://user:pass@server/db?

    • We could check class_exists or $this->has() to see if the given string represents a class or a value.
    • We could have some way of tagging an entry as a value.

    Right now, I prefer explicit signals over trying to “figure out” a programmer’s intent. So to explicitly tag this as a value, we’ll use a callable (such as an arrow function) that will return the value we want. Let’s revisit our configuration with this:

    $config = [
      //...
      DatabaseService::class => [
        'connectionString' => fn() => 'db://user:pass@server/db', // This is clearer.
      ]
    ];
    

    Then we’ll look for callables in the container:

    class Container implements Psr\Container\ContainerInterface {
      //...
      private function instantiateService(string $id) {
        // Get the listed dependencies from the container.
    		$args = array_map(
    			fn($dependency) =>
          	is_callable($dependency) ?
          		call_user_func($dependency) :
          		$this->get($dependency),
    			$this->config[$id]
    		);
    
    		return new $service(...$args);
      }
    }
    

    Level 3: Interfaces

    What about when a class takes an interface as a dependency (which it should)? Let’s add a PSR-18 HTTP client to one of our services:

    class UserService {
      public function __construct(
        private DatabaseService $db,
        private \Psr\Http\Client\ClientInterface $http,
      ) {}
      //...
    }
    

    Updating the UserService configuration is easy enough since an interface also has a class constant:

    $config = [
      //...
      UserService::class => [
        'db' => DatabaseService::class,
        'http' => \Psr\Http\Client\ClientInterface::class,
      ],
    ];
    

    But now we need to add ClientInterface to our container somehow. We need to have some way to give an implementation in the configuration; otherwise our container will (unsuccessfully) try to instantiate an interface!

    Going back to the idea of explicit signals, we actually can use strings here:

    $config = [
      //...
      \Psr\Http\Client\ClientInterface::class => MyHttpClient::class,
    ];
    

    Now we check the type of the class' configuration: if it’s a string, we get that class.

    class Container implements Psr\Container\ContainerInterface {
      //...
      private function instantiateService(string $id) {
        $config = $this->config[$id];
        
        if (is_string($config)) {
    			// This is an alias.
    			return $this->get($config);
    		}
        
        //...
      }
    }
    

    Note that we are very specifically not checking if $id is an interface. We want to be able to alias any class in here in case we want to replace a particular dependency with a subclass.

    We kind of handwaved an implementation of that class. What if we wanted to use something specific?

    Level 4: Factories

    Let’s say instead of rolling our own HTTP client, we used an off-the-shelf library like Guzzle?

    $config = [
      //...
    	\Psr\Http\Client\ClientInterface::class => \GuzzleHttp\Client::class,
    ];
    

    According to the Guzzle docs, a Client only needs a configuration array. We could do this with our existing config structure:

    $config = [
      //...
    	\GuzzleHttp\Client::class => [
        'config' => fn() => ['connect_timeout' => 30],
      ],
    ];
    

    And this would work! But there’s a small assumption here that could turn into technical debt.

    Remember that our container splats the configuration into the parameters of the class' constructor. If the maintainers of Guzzle ever change the name of the parameter from $config to something else, our container would break. One way to avoid this would be to remove the key from the dependency array, but that still feels fragile to me. What we need is a way to create an instance of Client without assuming it will have the same constraints our services have.

    We can do something similar to aliases: provide a callable function that returns the entire object.

    $config = [
      //...
    	\GuzzleHttp\Client::class =>
      	fn() => new \GuzzleHttp\Client(['connect_timeout' => 30]),
    ];
    

    Then we check for those in the container:

    class Container implements Psr\Container\ContainerInterface {
      //...
      private function instantiateService(string $id) {
        $config = $this->config[$id];
        
        if (is_callable($config)) {
    			// The config is a factory function.
    			return call_user_func($config);
    		}
        
        //...
      }
    }
    

    Finishing up

    At this point, we’ve hit all the use cases I have for a dependency injection container:

    • Lazy instantiation
    • One instance per class
    • Aliases (replacing one class/interface with another)
    • Dependencies can be other classes or functions returning a value
    • Factory methods to manually create instances

    There’s a few places we could go from here. We could use the Reflection API to automatically determine configuration for some simple cases. We could (should!) add more error handling for when the configuration doesn’t match the code. And if you need those features, you can build them! Or just use something off-the-shelf that already does it.

    Anyway, here’s our completed configuration and container:

    // Service Classes //
    
    class DatabaseService {
      public function __construct(string $connectionString) {
        $this->connection = new DatabaseConnection($connectionString);
      }
      //...
    }
    
    class UserService {
      public function __construct(
        private DatabaseService $db,
        private \Psr\Http\Client\ClientInterface $http,
      ) {}
      //...
    }
    
    class UserApiService {
      public function __construct(private UserService $users) {}
      //...
    }
    
    // Configuration //
    
    $config = [
      UserApiService::class => [
        'users' => UserService::class,
      ],
      UserService::class => [
        'db' => DatabaseService::class,
        'http' => \Psr\Http\Client\ClientInterface::class,
      ],
      DatabaseService::class => [
        'connectionString' => fn() => 'db://user:pass@server/db',
      ],
    	\Psr\Http\Client\ClientInterface::class => \GuzzleHttp\Client::class,
    	\GuzzleHttp\Client::class =>
      	fn() => new \GuzzleHttp\Client(['connect_timeout' => 30]),
    ];
    
    // Dependency Injection Container //
    
    class Container implements Psr\Container\ContainerInterface {
      private array $instances = [];
      
      public function __construct(private array $config) {}
    
      public function has(string $id): bool {
        return array_key_exists($id, $this->config);
      }
      
      public function get(string $id) {
        // Check if $id is in the configuration.
        if (!$this->has($id)) { throw new ServiceNotFoundException($id); }
        
        // If we don't already have an instance, create one.
        $this->instances[$id] ??= $this->instantiateService($id);
        
        // Return the instance.
        return $this->instances[$id];
      }
      
      private function instantiateService(string $id) {
        $config = $this->config[$id];
        
        if (is_callable($config)) {
    			// The config is a factory function.
    			return call_user_func($config);
    		}
        
        if (is_string($config)) {
    			// This is an alias.
    			return $this->get($config);
    		}
    
        // Get the listed dependencies from the container.
    		$args = array_map(
    			fn($dependency) =>
          	is_callable($dependency) ?
          		call_user_func($dependency) :
          		$this->get($dependency),
    			$config
    		);
    
    		return new $service(...$args);
      }
    }
    

    I’ll leave you with this last comment. You’ll note that our simple container still adheres to the Psr\Container\ContainerInterface interface. When I’m building a service that needs a container, I’m depending on this interface, not my specific container. The only part of Smolblog that really cares about how the container works is this configuration. And because this configuration is itself so simple, I could adapt it to a different container if and when I need to.

    Which is really the whole point of this exercise: loosely couple things together. Using standard interfaces and a dependency injection container means that many of the key libraries Smolblog depends on can be swapped out. And that includes the container itself.

    Thanks for reading; I’ll see y’all next time.

    → 8:59 PM, Sep 1
  • TFW you type docker compose stop when offboarding a client.

    → 9:17 AM, Sep 1
  • I do not have all the time in the world, and my goals for Smolblog pretty much require it to be in PHP right now: it’s efficient, type-safe, well-supported, and a language I and many others know well.

    But if I had time… I really wonder what it would look like as server-side Swift.

    → 9:58 PM, Aug 31
  • There’s a lot of chatter about measuring the productivity of software engineers, especially at work right now. This article gets to the heart of it: most metrics measure effort and output when we should be measuring outcome and impact.

    → 6:52 PM, Aug 30
  • As an exercise, I tried seeing if I could make a quick middleware for adding content provinance manifests to media uploaded to Smolblog. And I think I’ll call this concept proved.

    → 8:05 PM, Aug 27
  • The studios’ (including Apple) behavior in these WGA and SAG/AFTRA strikes is… petty. Vindictive.

    I’m really starting to buy the narrative that streaming is an across-the-board bomb. They can’t give viewer numbers to the unions because the shareholders would revolt.

    → 6:45 PM, Aug 26
  • Smolblog is now (mostly) Micropub compliant. The exceptions are undeleting (may not implement), multiple photos on the same request (can’t get that to work in WordPress) and some incorrect authentication responses (again, WordPress).

    → 10:33 PM, Aug 24
  • Use Laravel’s Illuminate Database Query Builder With WordPress

    I’ve been working on Smolblog, a social web blogging app. To help me get to a minimally viable product sooner, I’ve been building it on top of WordPress. However, WordPress is built exclusively for the MySQL database, and I eventually want Smolblog to work with many different databases, especially SQLite. This means, for my own code, I need to abstract the database away.

    The first pass I had at this was to simply have Query objects and services to handle those. This would effectively abstract away the entire data layer, making it completely system-agnostic. It wouldn’t even need to be a traditional database. But as I built this system out, I was making more and more assumptions about what the database and data code would look like. And while the database code was all abstracted away, I still had to write it. A lot of it. And every line I wrote using $wpdb was another line I’d have to rewrite someday.

    I’ve been looking at other frameworks to use, and Laravel is by far the strongest contender. Their approach to dependency injection and services seems to line up well with how I’ve organically built Smolblog to this point. So when I found out that their database abstraction layer also included a way to use the library without taking on the entire Laravel framework, I decided to make “someday” today.

    Prerequisites

    • Composer: While you can use this library without using Composer, it’s very much not recommended. That being said, if you’re using this in a plugin for general use or otherwise don’t have control over your entire WordPress environment, be sure to use Mozart or some other tool to isolate the namespaces of your dependencies.
    • Populated database constants: Some of the more modern WordPress setups use a connection string or other way to connect to MySQL. I didn’t find a way to get that information out of the $wpdb constant, so this code relies on having DB_HOST and other constants from wp-config.php defined.
    • PDO::MySQL: Illuminate DB uses PDO to handle databases, so you’ll need to make sure your PHP server has the PDO::MySQL extension installed. I’m using the official PHP image, so I needed to add these two lines to my Dockerfile:
    RUN docker-php-ext-install pdo_mysql  
    RUN docker-php-ext-enable pdo_mysql
    

    Step 1: Dependency Injection

    We’re going to use dependency injection to separate creating the database connection from using the database connection. This way the database connection can change without as much code changing.

    The documentation for Laravel’s query builder involves calling their DB facade, a global class that calls a singleton instance. Digging through the documentation and code, it looks like the underlying class conforms to the Illuminate\Database\ConnectionInterface interface. So that’s what we’ll use in our service’s constructor:

    use Illuminate\Database\ConnectionInterface;
    
    class ContentEventStream implements Listener {
    	public function __construct(
    		private ConnectionInterface $db,
    	) {
    	}
    }
    

    Inside the service, we’ll follow the documentation, replacing any use of the DB facade with our $db object:

    $this->db->table('content_events')->insert(['column' => 'value']);
    

    Step 2: Connection Factory

    Now that we know what we need, we need to create it.

    The README for the Illuminate Database package has good starting instructions. We’ll combine those with data from wp-config.php and $wpdb:

    use Illuminate\Database\Capsule\Manager;
    use Illuminate\Database\ConnectionInterface;
    
    function getLaravelConnection(): ConnectionInterface {
    	global $wpdb;
    
    	$capsule = new Manager();
    	$capsule->addConnection( [
    		'driver' => 'mysql',
    		'host' => DB_HOST,
    		'database' => DB_NAME,
    		'username' => DB_USER,
    		'password' => DB_PASSWORD,
    		'charset' => DB_CHARSET,
    		'prefix' => $wpdb->prefix,
    	] );
    
    	return $capsule->getConnection();
    }
    

    (As mentioned, we’re pulling the connection information straight from configuration. If you know how to get it from $wpdb, let me know!)

    The prefix property on the connection works much the same way as WordPress' table prefix. Since we’re using the connection object to also build our queries, it will add the prefix to our queries automatically. Using this property will also use the correct tables for blogs in multisite, so data from one blog doesn’t leak into another.

    For Smolblog, I only want one set of tables regardless of multisite. I also want to prefix the Smolblog-specific tables, mostly so they’re all in one place when I’m scrolling. So my prefix property looks like this:

    $capsule->addConnection( [
    	// ...
    	'prefix' => $wpdb->base_prefix . 'sb_',
    ] );
    

    Because I don’t want a global object or the Eloquent ORM, I can ignore the rest of the setup from the project README.

    Finally, we’ll want to store this created object somewhere central. Smolblog uses a simple dependency injection container, so we’ll store it there. The first time a service that needs a database connection is created, the container will run this function and provide the object.

    (Honestly, the container probably deserves a blog post of its own; you can look at the source code in the meantime.)

    Step 3: Update the Schema

    We have our code to build queries. We have our connection to the database. The only thing we need now is the actual tables for the database.

    Here is where we can use WordPress to its full extent. We will be using the dbDelta function in particular. This will tie into WordPress' existing system for updating the database structure alongside WordPress itself.

    Some plugins tie this migration code to an activation hook, but we want to be able to modify the tables even after the plugin is activated. So our process will look like this:

    1. Loop through the different tables we will need.
    2. Check the blog options for a schema version.
    3. If the version matches what we have in code, we’re up-to-date. Skip to the next table.
    4. Pass the new table schema to dbDelta and let it do its thing.
    5. Save the schema version to blog options.
    6. Rinse and repeat for each table.

    At this point, I should bring up some of the caveats with the dbDelta function. The comments on the WordPress documentation are invaluable here, especially as they point out a few things that need to be consistent with our schemas.

    Because there’s so many things that need to be consistent, we’ll isolate the unique parts of our table schemas to two things:

    1. A name. Because every table needs one. We will declare it without the prefix.
    2. The fields excluding the primary key. We can have UNIQUE indexes on other fields for a similar effect, but every table will have an auto-incrementing id field.

    A series of values keyed to short strings? That sounds like an array! Here’s part of what Smolblog’s schema array looks like:

    class DatabaseHelper {
    	public const SCHEMA = [
    		'content_events' => <<<EOF
    			event_uuid varchar(40) NOT NULL UNIQUE,
    			event_time varchar(30) NOT NULL,
    			content_uuid varchar(40) NOT NULL,
    			site_uuid varchar(40) NOT NULL,
    			user_uuid varchar(40) NOT NULL,
    			event_type varchar(255) NOT NULL,
    			payload text,
    		EOF,
    		'notes' => <<<EOF
    			content_uuid varchar(40) NOT NULL UNIQUE,
    			markdown text NOT NULL,
    			html text,
    		EOF,
    	];
    
    	public static function update_schema(): void {
    		foreach ( self::SCHEMA as $table => $fields ) {
    			self::table_delta( $table, $fields );
    		}
    	}
    
    	//...
    }
    

    A brief aside: Smolblog uses UUIDs for its unique identifiers, and they’re stored here as full strings in fields ending with _uuid. I ran into trouble storing them as bytes, and something in WordPress would frequently mess with my queries when I had fields named things like user_id and site_id. I’m noting this here in case you run into the same things I did.

    When WordPress loads the plugin, it will call the update_schema function declared here. That function loops through the array, extracts the table name and fields, and passes them to this function:

    public static function table_delta( string $table, string $fields ): void {
    	global $wpdb;
    
    	$table_name      = $wpdb->base_prefix . 'sb_' . $table;
    	$charset_collate = $wpdb->get_charset_collate();
    
    	$sql = "CREATE TABLE $table_name (
    		id bigint(20) NOT NULL AUTO_INCREMENT,
    		$fields
    		PRIMARY KEY  (id)
    	) $charset_collate;";
    
    	if ( md5( $sql ) === get_option( $table . '_schemaver', '' ) ) {
    		return;
    	}
    
    	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    	dbDelta( $sql );
    
    	update_option( $table . '_schemaver', md5( $sql ) );
    }
    

    This function takes care of the boilerplate we talked about earlier and runs the steps:

    1. It creates the table name using the same pattern as before: the base prefix plus sb_.
    2. It creates a CREATE TABLE SQL statement using the table name and fields. (It’s okay to build a SQL query this way because all of the data is coming from constants inside the PHP file; none of it is coming from form data or other untrusted sources.)
    3. It takes the MD5 hash of the SQL statement and compares that to the saved option for this table. The hash will change when the code changes, so this is a quick way to keep our code and database in-sync.
    4. If the database needs to be updated, it requires the correct file from WordPress Core and runs the dbDelta function.
    5. Finally, it saves the MD5 hash to the blog options so we know what version the database is on.

    By calculating the version using the hash of the actual SQL, we don’t have to worry about whether some other version number has been updated. This may or may not be the approach you want to take in a production application, but it has proven very useful in development. This is the same idea as using the filemtime function as the “version number” of static CSS and JavaScript in your theme.


    So there we have it. We’ve used the connection information in WordPress to hook up a Laravel database connection. And at some point in the future, it’ll be that much easier to let Smolblog work with SQLite which will in turn let Smolblog work on even more web hosts. And you can use this to do whatever you want! Maybe you just wanted to transfer some skills from Laravel to WordPress. Maybe you’re just in it for the academic challenge.

    One thing you can do with this is unit-test your services using an in-memory SQLite database… and I’ll leave you with that.

    final class DatabaseServiceUnitTest extends \PHPUnit\Framework\TestCase {
    	private \Illuminate\Database\Connection $db;
    	private DatabaseService $subject;
    
    	protected function setUp(): void {
    		$manager = new \Illuminate\Database\Capsule\Manager();
    		$manager->addConnection([
    			'driver' => 'sqlite',
    			'database' => ':memory:',
    			'prefix' => '',
    		]);
    		$manager->getConnection()->getSchemaBuilder()->create(
    			'content_events',
    			function(\Illuminate\Database\Schema\Blueprint $table) {
    				$table->uuid('event_uuid')->primary();
    				$table->dateTimeTz('event_time');
    				$table->text('payload');
    			}
    		);
    
    		$this->db = $manager->getConnection();
    		$this->subject = new DatabaseService(db: $this->db);
    	}
    
    	public function testItPersistsAContentEvent() {
    		$event = new class() extends ContentEvent {
    			public function __construct() {
    				parent::__construct(
    					id: Identifier::fromString('8289a96d-e8c7-4c6a-8d6e-143436c59ec2'),
    					timestamp: new \DateTimeImmutable('2022-02-22 02:02:02+00:00'),
    				);
    			}
    
    			public function getPayload(): array {
    				return ['one' => 'two', 'three' => 'four'];
    			}
    		};
    
    		$this->subject->onContentEvent($event);
    
    		$expected = [
    			'event_uuid' => '8289a96d-e8c7-4c6a-8d6e-143436c59ec2',
    			'event_time' => '2022-02-22T02:02:02.000+00:00',
    			'payload' => '{"one":"two","three":"four"}',
    		];
    
    		$this->assertEquals((object)$expected, $this->db->table('content_events')->first());
    		$this->assertEquals(1, $this->db->table('content_events')->count());
    	}
    }
    
    → 9:04 PM, Aug 22
  • This can’t be good…

    A picture of an iPhone 8 that is swollen on one side, indicating a failed battery.
    → 12:09 PM, Aug 21
  • Looking forward to PubKit; definitely something the fediverse needs.

    → 12:56 PM, Aug 19
  • Follow-up to last night’s gripe: on a Mac, there’s no difference between PictureDeleted.php and PIctureDeleted.php. On Linux (and therefore GitHub Actions), there is.

    → 10:21 AM, Aug 18
  • Sometimes you gotta call it a night, even when the tests run fine on your machine but for some reason Github Actions doesn’t see one of your classes.

    → 10:10 PM, Aug 17
  • Been thinking about streaming services lately...

    Screenshot of the show 'Game Changer' from CollegeHumor saying: Streaming is a game changer. Tonight's guests...Screenshot with Josh Reuben saying: Formerly Major League Baseball, it's Disney Plus, launched 2019.Screenshot with Zac Oyama saying: You're gonna pay more for sports, it's Peacock, launched 2020Screenshot with Brennan Lee Mulligan saying: Legally, it's a brand new service, it's Max, launched 2023.Screenshot with Sam Reich captioned: Dropout, launched 2018. Implied is Sam's catchphrase: I've been here the whole time.

    → 10:29 AM, Aug 13
  • 4 years ago I wanted a gaming PC. Bought an Alienware because I didn’t want to go through the hassle of buying/assembling compatible parts.

    Now I can’t upgrade because Dell uses proprietary motherboards. So it looks like I’m building.

    → 1:23 PM, Aug 12
  • Taliesin Jaffe’s face—as Sam Riegel does his weekly ad-read—is the epitome of “It’s so nice to see other people having to hear this.”

    → 10:19 PM, Aug 10
  • RSS
  • JSON Feed
  • Surprise me!
  • Also on:
  • Twitter
  • Tumblr
  • Micro.blog
  • Mastodon
  • LinkedIn