• Projects
  • Archive
  • Links
  • I’ve been wanting to use readonly classes since PHP 8.2 came out a year ago, but I’ve been blocked by my (to this point) loosey-goosey approach to immutability and PHPCS not supporting the syntax.

    Turns out PHPCS was Going Through Some Things. It’s better now.

    → 10:30 AM, Dec 29
  • Fun game over on Bluesky that I wanted to share: what’s the best single line from a song? Most of the ones I could think of were whole bridges or only worked in context. Finally settled on an older lyric from The Echoing Green.

    Screenshot of a Bluesky post. An image says: Best single line from a song; Not best chorus, not best verse, not best bridge, Just one line. Evan’s response: And I’ll believe for everyone that love has come and sorrow has an end.
    → 12:19 PM, Dec 28
  • Is there some “dev mode” in Mastodon I can turn on to see (a) what ActivityPub messages are being received and (b) why they aren’t doing anything/got rejected?

    → 5:27 PM, Dec 27
  • After trying and failing multiple times to get multiple Fediverse projects working inside my existing Docker Compose setup, I’m biting the bullet and paying for another VM so I can finally test against Mastodon.

    I feel like I’m missing something…

    → 9:25 PM, Dec 26
  • For those that celebrate, hope you are having a wonderful Christmas. Today’s joy is a six-sided keyboard switch tester that doubles as a fidget toy.

    I will try not to annoy absolutely everyone. No promises.

    Evan’s left hand holding a cube with a keyboard key on each side.
    → 12:04 PM, Dec 25
  • Bookmarking this post comparing social media site interactions to UX dark patterns for the eventual essay on social media.

    → 9:02 PM, Dec 24
  • Testing time…

    A series of post titles that, in reverse order, reads as the chorus and second verse to “Vertigo” by U2.
    → 8:55 PM, Dec 21
  • From @brentsimmons’ post on adding Mastodon support to the RSS reader NetMewsWire:

    Such an app could be a nice unified experience. Get your Mastodon, Threads, RSS feeds, Micro.blog and, hopefully, other services — anything that supports ActivityPub, RSS, or some other open format or API — all in one place, in a way that’s already familiar to everyone.

    That’s the second half of the plan for Smolblog. It’s under Beta Phase 2. Feedback and help is always appreciated.

    → 11:57 PM, Dec 17
  • Just wrote the last sentence of a story that comes in at just under 12k words.

    But I like this one too much, so it’s getting edited!

    → 12:26 AM, Dec 17
  • Bookmarking this article about moving from CRUD to Event Sourcing as it has some of the simplest explanations of the concept I’ve seen.

    → 7:10 PM, Dec 16
  • Fascinated by this talk by Gary Bernhardt about boundaries in software architecture. It’s validated some of the architecture I’ve used with Smolblog and given me ideas for what I haven’t…

    → 4:25 PM, Dec 7
  • There are always going to be the free riders – tech companies, perhaps – who benefit from the interconnectedness of the world while refusing to do the work.

    — Duke University Libraries in a post aptly titled “Why We’re Dropping Basecamp”

    → 10:15 PM, Dec 4
  • Last year Fortnite did a big in-game event to end Chapter 3 and launch Chapter 4. This year they had even worse queuing (my wife got kicked out an hour before and couldn’t get back in) just to have a few cinematic teasers and… get kicked out anyway I guess? I’m not mad, just disappointed.

    → 3:23 PM, Dec 2
  • Sometimes you get an error that just makes you want to walk into the ocean.

    Screenshot with white text on a red background: Error from Smolblog: Call to a member function toString() on string
    → 10:35 PM, Nov 30
  • I hate signing into my Google account on my phone. Because Google, a company run by Very Smart People, always assumes I’m signing into my phone for the first time and bombards me with emails and notifications about “setting up” my phone and “making it better” with all of their apps. 🙄

    → 9:21 AM, Nov 30
  • It goes like this:

    1. Find out my company is using a product.
    2. Look at product.
    3. Find out they have a free tier.
    4. Sign up for said free tier.
    5. Find out that this product allows public comments on a roadmap.
    6. Post the link to the Smolblog roadmap I just created.
    7. Ask people to like, comment, and subscribe.
    → 6:40 PM, Nov 29
  • And after resigning myself to just using a raw SQLite database to store my character/setting info, I gave Collections Database a try. So far it’s working well enough to spend the $7 on the pro version…

    → 8:22 PM, Nov 28
  • Started and stopped several attempts last night at finding some software to use as a character database… Ended up making a SQLite file and opening it in TablePlus.

    → 9:49 AM, Nov 28
  • Hey Stripe, what’s your black Friday dashboard say about the number of businesses having their “best day ever”?

    Screenshot from the Stripe public dashboard. The entry for businesses having their best day ever on Stripe is 9,001.
    → 10:44 AM, Nov 24
  • If you want to upload a file through an HTML form, be sure to set the content type to multipart/form-data. Otherwise it will not work.

    If you want to upload a file through the JavaScript fetch function, be sure NOT to set the content type manually. Otherwise you’ll be stuck for a week. Like me.

    → 2:28 PM, Nov 22
  • Not me belting out a line from the song stuck in my head and pulling up to the drive thru window just to be told the mic was still hot. You must have me confused with someone else.

    → 10:07 PM, Nov 15
  • Some belated Veterans Day words from a colleague who passed earlier this year:

    Please be mindful that there are folks that you know who are holding a lot in their hearts. And this weekend, their hearts may feel particularly heavy.

    → 11:23 AM, Nov 13
  • Hearing rumblings of Tumblr being put on life support. I’m trying not to let it pressure me…

    But come ON! I had two API integrations ready to start with Smolblog. But Twitter doesn’t exist anymore, and now Tumblr?!

    Anyway, thanks @manton @jean @vincent and co for the stable platform.

    → 1:06 PM, Nov 9
  • I’ve been planning on using the AGPLv3 license for Smolblog and selling exception licenses. It felt like the best way to keep the project Actually Open Source™ without leaving it open to poaching by major cloud players. And now Element (co. behind Matrix) is providing some major vindication.

    → 10:44 AM, Nov 7
  • Some days you get an idea. Some days that idea doesn’t last.

    Screenshot from Mastodon. Evan asks: if the finger tap for the Apple Watch only works when it’s being looked at, does that mean it can’t be used as a no-look slide clicker? John Gruber responds in the affirmative.
    → 2:23 PM, Nov 3
  • Redownloaded the Tetris app last night. Opened it this morning, clicked past the login prompt, hit the giant play button, got a few triples, and apparently beat the level?? Which did some bonus animation and then played an ad???

    I have uninstalled the Tetris app.

    → 8:55 AM, Nov 2
  • Finally checked my PO Box after way too long. Thanks for the sticker, @jean!

    A Micro-dot-blog sticker on top of a laptop next to a Smolblog sticker
    → 9:07 AM, Nov 1
  • “Lisdexamfetamine” scans to “God Bless America.” I can do no more with this cursed knowledge.

    → 10:35 AM, Oct 31
  • Something I haven’t seen offered for the Apple “Scary Fast” event: the new whole-console CarPlay they showed off last year.

    Another possibility: F1 on Apple TV. 🏎️

    → 2:40 PM, Oct 27
  • Said goodbye to our dog Toby yesterday. We’ve had him since he was 3 months old, and he’s literally grown alongside us for the last 13 years. The weirdest, strangest, goodest boy. May he rest in peace. More pics on Instagram.

    Portrait photo of TobyPicture of me eating a gyro, an empty plate on my lap, while Toby looks on, shocked that I am not giving it to him.Toby leaping up in the snow

    → 10:19 AM, Oct 27
  • Nothing like a Sunday afternoon at the emergency vet, but when your normally lethargic dog can’t lie still because of the pain, you go.

    → 3:53 PM, Oct 22
  • Nothing like getting an alert and seeing that whoever set it up set the threshold level to 9000…

    → 10:05 PM, Oct 12
  • Anyone know of an end-user friendly cross-platform/cross-browser guide to installing a web app?

    Google has plenty of guides… for Chrome. And MDN doesn’t get into the details (and isn’t updated for macOS Sonoma yet).

    → 8:16 AM, Oct 12
  • My old coworker Lax has been on his own journey of self-discovery and self-improvement, and he’s written a post summarizing what he’s learned so far. Having seen his growth is inspiring.

    → 10:11 AM, Oct 2
  • I’ve got a poll running on my Mastodon asking how people are getting long form content these days. Would love your input.

    → 3:24 PM, Oct 1
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • Hot take: “Don’t Fear the Reaper” actually does need more cowbell.

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

    → 5:11 PM, Sep 10
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • TFW you type docker compose stop when offboarding a client.

    → 9:17 AM, Sep 1
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • Looking forward to PubKit; definitely something the fediverse needs.

    → 12:56 PM, Aug 19
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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
    Also on Bluesky
  • 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.

    → 8:47 AM, Aug 6
    Also on Bluesky
  • 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.'

    → 9:56 PM, Jul 29
    Also on Bluesky
  • 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…

    → 7:12 PM, Jul 27
    Also on Bluesky
  • 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/…

    → 10:51 PM, Jul 25
    Also on Bluesky
  • 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’
    → 10:42 AM, Jul 24
    Also on Bluesky
  • 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.’

    → 9:33 AM, Jul 19
    Also on Bluesky
  • 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!

    → 1:06 PM, Jul 17
  • 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?

    → 7:24 PM, Jul 12
  • 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.

    → 10:54 PM, Jul 8
  • Ok, anyone got any good Micropub clients I should test against?

    → 1:08 PM, Jul 7
  • 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.”
    → 9:37 PM, Jul 6
  • 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)

    → 5:07 PM, Jul 6
  • 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.

    → 3:30 PM, Jul 6
  • 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 😇
    → 10:47 AM, Jul 6
  • 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.”

    • It’s our content, so we can sell access to AI models.
    • It’s our content, so we should block access to it.
    • It’s our content, so why should app makers freeload?
    • It’s our content, so why should app makers freeload?
    • It’s our platform, so why should app makers freeload? (If the shoe fits…)

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

    → 11:01 PM, Jul 1
  • To be clear: I don’t envy the level of scale Twitter has to operate at, and I appreciate the very difficult problems that brings.

    But I have no sympathy when all of the infrastructure and people set up to deal with said problems were deemed unnecessary.

    → 2:26 PM, Jul 1
  • HAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHA

    (inhale)

    AHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHAHA

    Tweet from Elon Musk saying: To address extreme levels of data scraping & system manipulation, we’ve applied the following temporary limits:&10;&10;- Verified accounts are limited to reading 6000 posts/day&10;- Unverified accounts to 600 posts/day&10;- New unverified accounts to 300/day
    → 2:12 PM, Jul 1
  • As someone who’s followed them since 1992, I feel a particular thrill knowing Apple’s worth three trillion dollars now. I’d love to see them just… solve a problem. Housing, child poverty, something real and big.

    → 6:25 PM, Jun 30
  • Been isolating the data layer from the core domain model, largely because I know I’ll have to redo it post WordPress. But it’s starting to feel like there’s too many implicit assumptions.

    So I’m just going to go ahead and use Laravel’s DB library. Worst case, I have to redo everything anyway.

    → 10:28 AM, Jun 30
  • No, I didn’t dependency-inject a microformats parser into the class just so I could call it $this->mf.

    That was one of the reasons, yes, but not the only reason.

    → 10:43 PM, Jun 25
  • Ok, so ADD-brain resurrected the “Beef: it’s what’s for dinner” ads a month ago in my brain.

    And then I just saw a new one during the baseball broadcast.

    What.

    → 7:41 PM, Jun 24
  • Looks like Tumblr is selling domains now to the exclusion of external domains. I’m about 50/50 on whether this goes well or not.

    → 11:55 AM, Jun 20
  • OK, Internet, you have very rarely disappointed me. So now I ask:

    Did anyone manage to save the Newsboys' Live From the Airdome concert from the year 2000? It was a live broadcast and on-demand for about a month after in Real Player.

    → 10:56 AM, Jun 17
  • Y’know, I was afraid I was going to miss the Twitter cross-posting, but three out of four likes on my latest post are spam.

    Anyway, I’m @oddevan on micro.blog which will stop cross-posting in a month.

    → 9:46 AM, Jun 13
  • Is there a Mastodon server out there that people use for testing federation? Or do I just grab a docker image and spin one up?

    → 5:27 PM, Jun 11
  • Gotta say, thoughts about constant advertising aside, I love when it takes advantage of the medium. Case in point: Duracell sponsoring Williams Racing in Formula 1. 🏎️

    Williams Racing Formula 1 car. On the top of the car, just above the driver's head, is a perfectly placed advertisement such that it looks like a Duracell Coppertop AA battery.
    → 10:14 AM, Jun 3
  • If there’s one thing the internet has taught me, it’s that there’s always someone else. Knowing we aren’t alone in whatever thing is such a powerful feeling.

    Anyway, happy Pride Month to all who celebrate.

    → 8:53 PM, Jun 1
  • If I had any doubts about making the Smolblog app a Progressive Web App, they’re gone now.

    (via @cassidoo@notacult.social’s weekly newsletter)

    → 8:47 AM, May 15
  • I know this is the 1% of First World Problems… but I really don’t get recruiters that send solicitations to my work email. Like, y’all know my bosses could read that, right?

    → 10:45 AM, May 12
  • And we are officially at the stage of ruthlessly cutting anything that can be cut for a Minimally Viable Product.

    Latest deferment is custom permalinks.

    I’ve got time off in a week and a half and I want to be able to work on implementing ActivityPub then!

    → 10:34 PM, May 6
  • I really hope these plants turn out well. We just planted a lemon tree, a poblano, and a mint.

    I also hope the Organic Fertilizer we got washes out. 💩

    → 8:51 PM, Apr 30
  • No, you’re writing a form builder because the thought of all that boilerplate is more daunting than writing a DSL and the components to go with it. 🫠

    → 2:54 PM, Apr 23
  • Bookmarking this older Mastodon blog post as an example of how an AGPL-licensed project can handle someone using the code in an undesirable way.

    → 4:27 PM, Apr 15
  • Pour one out.

    Hello, This is a notice that your app - Smolblog - has been suspended from accessing the Twitter API.  Please visit developer.twitter.com to sign up to our new Free, Basic or Enterprise access tiers.  More information can be found on our developer community forums.  Regards,  Twitter Developer Platform
    → 6:31 PM, Apr 14
  • First world problem here: my Jeni’s Pint Koozie kept slipping off my ice cream. Started to wonder if the elastic was finally giving out.

    Finally realized the koozie is fine; Häagen-Dazs skimped out on their “pints” and made them 14oz instead of 16oz. 😡

    → 10:10 PM, Apr 11
  • Feels like I’m getting close to what I would call an MVP for Smolblog. Currently wiring up some projections and trying to event-source my way into making WordPress do what I want.

    This is either madness or brilliance…

    → 11:58 PM, Apr 8
  • It looks like the new Twitter rules mean Micro.blog will stop posting to Twitter. So… yeah. See y’all… somewhere else.

    → 2:05 PM, Mar 30
  • My iOS app was released ten years ago today. It was first written between jobs amidst a lot of uncertainty. But it was my first step towards the kind of programming job I wanted. Anyway, it’s updated and you should check it out.

    → 2:13 PM, Mar 27
  • Took a mental health day to recharge and clean a bit, so meet the new desktop! New this time: Air96 keyboard from NuPhy and a Sonic v Shadow deskmat from Higround.

    photo of my physical desktop
    → 9:07 PM, Mar 23
  • YouTube TV: Hey, we’re losing MLB network.
    Me: Any price adjustments?
    YTTV: Yeah, it’s going up by $8.
    Me: …hey, Siri?
    Siri: Yes?
    Me: Play “Won’t get fooled again.”

    → 5:20 PM, Mar 16
  • Sometimes it’s fun to see what I can do in one evening with my exclusively self-taught skills in Motion.

    If only actually making the videos the graphics are for wasn’t so much work.

    Anyway, have a Smolblog bumper.

    → 10:32 AM, Mar 15
  • Heard that there are plans in place to take care of SVB’s customers. As someone employed by one of those customers… thanks.

    There need to be consequences for those responsible for this, not for those affected.

    → 9:28 PM, Mar 12
  • This FreeCell app gives 10 points for moving cards to the foundation but deducts 5 for every other move. But your score can’t go below zero. So every move with 0 points is free.

    So if you turn off auto-solve and manage to sort everything…

    → 7:15 PM, Mar 4
  • Heads up if you had an active Tweetbot or Twitterific subscription, the developers are beginning refunds. For Tweetbot, you can transfer to Ivory or opt-out entirely.

    I’ll encourage anyone that can to opt-out of a refund in support of these small businesses.

    → 5:44 PM, Mar 1
  • Okay, since LocalWP still doesn’t have PHP 8.0 (much less 8.2!), what’re people using these days for WordPress development?

    → 3:13 PM, Feb 25
  • How are people getting their taxes done these days? Just found out my usual accountant isn’t available this year.

    → 12:10 PM, Feb 16
  • And Miller/Coors/Blue Moon gets the first genuine LOL of the night.

    → 9:08 PM, Feb 12
  • So on Twitter I got a lot of news by osmosis. Anyone have any Micro.blog or fediverse accounts they recommend on that front? Or should I just start curating my Apple News again?

    I don’t want to be as angry as Twitter could make me, but I do want to keep an eye on things.

    → 11:39 AM, Feb 11
  • Does anyone know if the free Twitter API tier is one user per application? Or is it 1.5k tweets per user?

    → 8:46 PM, Feb 9
  • More Twitter changes! Except these might mean this blog might stop cross-posting. So you can also follow me at

    • @oddevan@micro.blog
    • oddevan.tumblr.com
    • status.oddevan.com

    🖖 DFTBA 🖖

    → 6:19 PM, Feb 2
  • What Discord do I join to discuss extensions to Markdown? 😉

    No but seriously, what do y’all think of using @ //url/to/embed as a block-level element to denote embedding remote content (like a YouTube vid or Tumblr post)?

    → 12:19 AM, Jan 31
  • 👀 thebloggess.com

    Screenshot of theblogess.com showing an ad for PillTimer
    → 9:21 PM, Jan 23
  • Just paid for my first text ad on a website. 😬

    → 9:34 PM, Jan 19
  • The last straw, I guess...

    I’m not leaving Twitter, but I’ll be using it a lot less like I used to.

    I wish I could leave outright, but I’ve got too many social connections on there that I don’t have elsewhere. People I’ve met in different places that haven’t moved to other places. (Or they have, and I just missed it—which is likely.)

    But after a few days of having to use the official app… it feels like Facebook. Or Instagram. A whole lot of stuff that I don’t care about, even when it purports to only show me what I care about (the “Following” timeline). And I don’t just mean advertisements, I mean:

    • Tweets from people followed by people I follow
    • Tweets liked by people I follow
    • Links to accounts that might be interesting to me for some reason
    • Out-of-order replies or truncated threads

    Throw in some obnoxious UI elements like a new tweet button and a banner proclaiming that there are tweets that I have not seen while I am indeed scrolling up to read said tweets, and I feel like I’m squinting to see through a haze of noise to see the one thing I came to see: tweets from people I follow.

    People with more experience in the world of Twitter apps (like Manton and Craig and John) have already said their things, but I’ve got two more data points to add.

    First, I recognize that this is a pretty pathetic straw to be the one to make me “leave” Twitter. Others have been dealing with rising bigotry, hate, and a general increase in… uncivil behavior on the platform. This has always been there, and it’s been there since well before Twitter was taken private. Since then, though, the platform leadership has made it clear that this will continue in the name of “free speech,” welcoming back some of the most prominent accounts that encouraged said vitrol.

    I want to note that this was something I knew was happening but rarely saw. I used Tweetbot instead of the official Twitter app, and it only showed my timeline in order along with any mentions or direct messages. Since someone of my race, religion, gender, and sexual orientation is not typically the target of hate and harassment campaigns, it was something I didn’t experience. But I followed enough accounts and heard enough stories to know that it was happening somewhere; I just didn’t see it.

    The problem is the more the Twitter app shows me what’s happening “on the platform” and less of who I’ve chosen—for the sake of my mental health—to listen to, the more I’ll see. I don’t even want to see trending topics much less whatever tweets people are angry about. Tweetbot allowed me to control what I saw on Twitter; the official Twitter app doesn’t. That is why these third-party clients were so important to me.

    Second, this has confirmed that Twitter’s new ad-hoc approach to policy also extends to the API. The theme repeated by app developers has been “respect,” or rather the lack thereof: the banning of 3rd-party apps was made with no notice beforehand and no acknowledgement after. It’s also inconsistent: several smaller apps, including the Mac desktop version of Twitteriffic, have yet to be banned. No respect, no communication, no consistency. If nothing else, this shows Twitter is not a stable platform for building on.

    Which brings me to Smolblog. I’ve been working on it off-and-on for years now, and one of the key features has always been a Twitter integration that will import posts from and send posts to Twitter. As part of the big refactor, I was rewriting this module to use the new version of Twitter’s API. It wasn’t until I was most of the way in that I learned that there are still key features—like posting images to Twitter—that are simply not available through version 2 of Twitter’s API.

    With the growing instability in the API, I simply cannot waste any more time building a feature on an API that is incomplete and could change at a moment’s notice. The change could be in a data structure, or it could be a policy change that would ban Smolblog. With me already feeling “behind schedule” (an attitude to examine later), I need to get core functionality out, and that means putting any Twitter work on the back burner.

    So there it is. Twitter stays on my phone, but it gets shoved into the back page with Instagram and Facebook: apps I only touch when I need to. And I’ll likely need to; there are people, things that I can still only get to through Twitter, and asking people to give up their hard-won audience is no easy ask. But this makes the mission of Mastodon, Micro.blog, and every other independent cross-compatible service that much more important. And I hope one day that Smolblog is in that list.

    Jack Dorsey was right about one thing for sure, though: Twitter should have been a protocol, not a company.

    → 9:58 AM, Jan 18
  • Shoutout to App Store review for the < 24h turnaround!

    Also PillTimer 3.1 is now live! I did a D&D reference in the update notes, but now I’m regretting not doing a retro Windows reference.

    → 12:31 PM, Jan 17
  • Maybe when Microsoft makes the new AI-powered Clippy, it’ll be able to do this? (via Bernice King on Twitter)

    Image of Clippy from Microsoft Office saying: Hi! It looks like you've quoted Martin Luther King, Jr. out of context instead of engaging with the complex reality of white supremacy in America. Would you like some help with that?
    → 10:22 AM, Jan 14
  • If this neutering of Tweetbot is intentional and not a mistake, I will be using Twitter much less.

    Regardless of intentionality, I’m halting the Twitter side of Smolblog for now. I don’t have time to waste on an API this unstable (whether by error or fiat).

    → 7:44 PM, Jan 13
  • Looks like it’s still a great day to not be able to use Tweetbot?

    error box saying failed to contact twitter
    → 9:36 AM, Jan 13
  • Microsoft doesn’t own the ending to Minecraft. It is now CC0-licensed and in the public domain.

    → 3:22 PM, Jan 6
  • Getting some serious Elasticsearch vibes from WotC’s proposed changes to DnD’s open-source-like license, except instead of medium co against ginormous co, it’s ginormous co vs. I guess everyone from individuals to CritRole??

    → 3:03 PM, Jan 5
  • I had to squeegee the inside of my windshield this morning.

    → 9:14 AM, Jan 5
  • Bookmarking this write up on the Channels app solely because I have wanted an app that will make a pretend cable channel out of my Plex library, and I thought I’d have to build it myself.

    → 6:38 PM, Jan 4
  • Under what conditions could the NFL, in good conscience, restart the game? (I know, I know, what conscience?) 🏈

    → 10:34 PM, Jan 2
  • Who is asking for the game to restart? It makes a huge difference for me whether it’s the players or the league.

    → 10:16 PM, Jan 2

Slightly uneven since 2005.

Find oddEvan on

  • Micro.blog
  • Bluesky
  • Mastodon
  • GitHub
  • Tumblr
  • YouTube
  • LinkedIn
  • Read.cv
  • TCGplayer

Projects

  • Smolblog
  • PillTimer
  • oddEvan UI
  • madcrasher
  • Other projects

Archive

Links

  • Blogroll
  • Resources
  • Fun Times

About

  • About Istoria

Colophon

Typeset in Raleway by The League of Movable Type and Satoshi by the Indian Type Foundry. Powered by Micro.blog. All your base are belong to us.

© Evan Hildreth; licensed under CC BY 4.0.