Deno But In PHP

Last updated on 13th September 2024

I come from a mostly-PHP server-side background. I’ve made a lot of things in a lot of languages, but PHP feeds my kids. Laravel is my framework of choice, not least of which because it gives me all the tools I need out the box. When I sit down to make a new app, I don’t have to think about what queuing library I’ll use, or how I’ll probably need to pay someone to get auth up and running in a reasonable amount of time.

But…I have always pressed up against the constraints of the PHP language. I have worked on projects that nobody in their right mind would use in production. Projects that expand the language or “abuse” it to do cool new things.

I want to do that again.

Brendt has been building his own framework, and I have found it inspiring. The road to a successful PHP framework is not an easy one; and I have no illusions of building something anywhere near as successful as Tempest or Laravel; but he has made me want to experiment again.

Here are a few things I want to try:

  1. File based routing, like Next
  2. Include-based dependencies, like Deno, with a plan for how others make dependencies like this
  3. First-party client-server bridge, like Livewire
  4. Good support for Tailwind + Alpine
  5. At least a plan for how to get the same set of tools that ship with Laravel
  6. Be 100% native PHP code, requiring no extensions or new syntax

It might be helpful to demonstrate some of what I’m thinking. For file-based routing, I was thinking something like this:

// ...init code

return $export(default: fn() => match($request()->wants()) {
  'json' => $response()->ok(),
  default => $view('index'),
});

This is from pages/index.php

This is an approach to sharing code in different files, while also being able to keep the global namespace mostly free of new classes, functions, and variables. I imagine export (or something similarly named) would take in classes, functions, and variables and allow them to be imported using something as simple as an require statement:

// ...init code

$router()->get('/', require 'pages/index.php');

This is from routes.php

This is what an explicit route would resemble, but I’d want to build in some file-based routing that wires this up internally. The export function would need to return something invokable:

return new class implements ArrayAccess {
  private Closure $default;
  private array $named;

  public function __construct(
    null|Closure $default = null,
    null|array $named = [],
  ) {
  }
    // ...do something with named non-default things
    // like checking that they are all classes and closures

    if (!is_null($default)) {
      $this->default = $default;
    }
  }

  public function __invoke(...$params) {
    if (/* default is a thing that can be invoked */) {
      return $this->default(...$params);
    }
    
    /* throw error */
  }

  // ...implement ArrayAccess
}

This is something that would live in the framework, and be accessible to all dependencies via the export function

ArrayAccess is interesting because it would allow things like:

$export = require 'export.php';
['request' => $request, 'response' => $response] = require 'http.php';
['view' => $view] = require 'view.php';

This is the “init” code from the earlier responder

This is obviously a bunch of boilerplate, and we need better ways of organising this, but it gives you an idea of how different this would be from normal PHP dev. For better or worse.

Another thing I’d like to consider is creating a standard organisation scheme for dependencies imported from URL paths. For example:

$export = require 'https://example.framework/[email protected]';

This would require a lot of thought, because it would essentially need to replace Composer as a dependency workflow. We’re talking about ensuring secure hosting, and being able to lock dependency versions and setup sturdy download caches.

I’m not sure people would want too adopt this model, but it’s essentially what you’re doing when you use an NPM dependencies without reading through all the source code on every npm install run.

In this way, the hosting and URL import scheme and export function become a spec that everyone can work to. Libraries can be useful without writing anything of their own to the global namespace. Multiple versions of a library can be used in the same project, without hack class re-definition or conflicts.

I don’t know how much of this is possible, but I do want to tinker with it. I need to thank Brendt for the inspiration, and Sam and Chris for their input and encouragement in these areas.