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.


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


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());
	}
}

This can’t be good…

A picture of an iPhone 8 that is swollen on one side, indicating a failed battery.

Looking forward to PubKit; definitely something the fediverse needs.


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.


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.


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.


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.


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


Nothing like getting up at 5:30 AM to clean up your dog’s forceful rejection of last night’s food.

Except maybe finding out he wasn’t done yet.


Usually you listen to the code style plugin.

But sometimes you need to remind it who’s boss. And disable stupid rules.

A code linter flagging a code comment saying 'Comments may not appear after statements.'An XML document excluding the rule in the previous image with a comment saying 'Seriously? Sometimes you just need a good comment on a line.'


Ok, anyone in my new social media orbit still play Animal Crossing on the switch? ‘Cause I’m hopefully set for some good turnip prices tomorrow afternoon…


It’s nothing but a mostly-blank Jekyll site right now, but I want to start collecting documentation for ActivityPub, Micropub, and other social web APIs, and I would love your help. github.com/smolblog/…


This isn’t even half-assed. Quarter-assed at best. I’d expect this from a smaller Google or Microsoft project that is under a new director with too much ego desperate to leave their mark and not enough people to tell them “no.”

Which, come to think of it… 🙄

The top of a page with an ‘X’ logo followed by the phrase ‘Sign in to Twitter’

It’s all about the little rituals.

Screenshot of Twitter with tweets from @_smolblog and @pilltimerapp both saying ‘Logging in so I don’t get deleted. Link in bio.’Screenshot of Twitter with tweets from [@oddevan](https://micro.blog/oddevan) and @madcrasher saying ‘Logging in so I don’t get deleted. Link in bio.’


Officially moved my micro.blog site to oddevan.com. I’m already doing the occasional longer post via micro.blog, so this is me absolving myself of keeping WordPress alive on the same box I use for Smolblog development.

Also, hello Bluesky!


The conversation around the impending thrediverse implies there’s this illusion of privacy people expect from Mastodon that just isn’t there. How would you communicate to a new user that content posted may end up on other servers, and there is no way to control what those servers do?


Your friendly reminder that the company telling us how many people are “joining” its “new” social network is the same company that dramatically overstated how many people were “watching” videos on their platform and bankrupted several companies by doing so.


Ok, anyone got any good Micropub clients I should test against?


Same, Piper; same.

Screenshot from Animal Crossing: New Horizons with Piper, a white bird, saying “No. Seriously. Some days, I have SO much to say.”

How these new chatbots are like TV psychics. Long article, but worth it. TL;DR: it has nothing to do with intelligence, either in the computer or in front of it. It’s basic machine learning and cognitive biases found in every human.

(via eevee on cohost)


I honestly find it a little refreshing when one of the bloggers I admire has a genuinely bad take; it means I still have my own opinions.

To wit: I don’t see why Meta can’t launch Threads in the EU except they insist on it being its own app.


Yes I signed up for an account on the sewing app. Mostly to

  1. get my name (though it turns out it’s just Instagram handles anyway so 🤷🏻‍♂️) and
  2. have another way to test federation on Smolblog 😇

Let's talk ownership

There’s a gentle tension underpinning sites for user-generated content.

User-generated content is kind of an archaic term now, I guess. I remember when it all started with Flickr, Digg, Delicious… (nostalgic sigh) Nowadays it’s “social media,” but the idea is still the same: websites whose purpose is to show things made by its users. Artwork on DeviantArt, photos on Instagram, blog posts on Tumblr, random thoughts on Twitter, et cetera.

Now, the user agreements very explicitly say that the sites do not own the content. That right, both legally and morally, remains with the actual author/artist. What the platform owns is the experience: who gets to see the content and under what circumstances. Sometimes users will chafe against this. Sometimes this is the value proposition; YouTube, for example, gives its creators a level of control over the advertisements and shares the resulting revenue. Patreon’s entire purpose is controlling access to content and providing a reliable experience for managing that control.

Users own the content, and platforms own the experience. As long as both parties know their roles in this dance, it can work beautifully. And when it doesn’t…

A content creator getting their own website is nothing new. (A lot of people—us?—in the indie web think it should happen more often, in fact.) It’s not for the faint of heart, but it’s not rocket science. I’d even say, at this point, it’s not even computer science; you can make a good website with several different tools now, none of which involve code.

But woe upon the platform that thinks it owns the content. The love of money is the root of all kinds of evils, and we’re seeing several of them right now.

So let’s talk about Twitter and Reddit.

At first glance, Twitter and Reddit’s decisions to charge for their API seems solidly in their wheelhouse. The API is part of the experience, right? It’s all about access, right?

Except look at the attitude expressed by Elon Musk and Steve Huffman (emphasis mine):

It costs a lot of money to run an app like Reddit. We support ours through ads. And what we can’t do is subsidize other people’s businesses to run a competitive app for free. [source]

Several hundred organizations (maybe more) were scraping Twitter data extremely aggressively… [source]

The Reddit corpus of data is really valuable… But we don’t need to give all of that value to some of the largest companies in the world for free. [source]

You will not… use or access the Licensed Materials to create or attempt to create a substitute or similar service or product to the Twitter Applications. [source]

And this is just what I could find. Go back through the interviews with these companies, and you’ll see a particular attitude start to emerge: “This is our content.”

The answer, of course, is that it’s not their content.