Oink: An API for PHP in a single file
github.com
github.com
Sometimes you just want to get shit done for your personal weekend hobby projects and little things like this can be a tremendous help to make your MVP. Sometimes these MVPs can be super practical in real-life as well (1)
Writing code this way can sometimes be liberating and more fun like freestyle swimming. I still miss the days of writing native SQL queries instead of using the ORM for everything nowadays.
(1) https://twitter.com/levelsio/status/1381709793769979906?lang...
This is whats really great about PHP, the time from "I have an idea" to a running implementation can be very quick and minimal. In a world of over-wrought solutions this still stands as a simple way to get shit running in a hurry.
Here is I how I do it with simple SQL and built in PHP constructs, easy to read, debug and implement.
First part of the example is class based and the second one function based if you are more into that.
<?php
declare(strict_types=1);
// SQL with classes
namespace {
$pdo = new PDO('sqlite::memory:');
$pdo->exec(
"CREATE TABLE article (
article_id INTEGER PRIMARY KEY,
name TEXT NOT NULL
)
");
$pdo->exec("INSERT INTO article (name) VALUES('My Article')");
interface Cache
{
public function get(string $key): mixed;
public function set(string $key, mixed $data): void;
}
final class RuntimeCache implements Cache
{
private array $cache = [];
public function get(string $key): mixed
{
return $this->cache[$key] ?? null;
}
public function set(string $key, mixed $data): void
{
$this->cache[$key] = $data;
}
}
}
namespace Article {
use Cache;
use PDO;
readonly class Article
{
public int $article_id;
public string $name;
}
interface ArticleRepository
{
public function getArticleById(int $article_id): Article|null;
}
final class SqlArticleRepository implements ArticleRepository
{
public function __construct(private readonly PDO $pdo)
{
}
public function getArticleById(int $article_id): Article|null
{
$stmt = $this->pdo->prepare(
"SELECT article_id, name
FROM article
WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetchObject(Article::class);
return $article === false ? null : $article;
}
}
final class CachedArticleRepository implements ArticleRepository
{
public function __construct(private readonly Cache $cache,
private readonly ArticleRepository $articleRepository)
{
}
public function getArticleById(int $article_id): Article|null
{
$key = "article-{$article_id}";
$cached = $this->cache->get($key);
if (!empty($cached)) {
return $cached;
}
$article = $this->articleRepository->getArticleById($article_id);
if ($article !== null) {
$this->cache->set($key, $article);
}
return $article;
}
}
}
namespace {
$repo = new Article\CachedArticleRepository(
new RuntimeCache(),
new Article\SqlArticleRepository($pdo)
);
var_dump($repo->getArticleById(1));
var_dump($repo->getArticleById(1));
}
// SQL with functions
namespace {
function curry(callable $f, ...$args): callable {
$rf = new ReflectionFunction($f);
$count = $rf->getNumberOfParameters();
return function (...$arguments) use ($f, $rf, $count, $args) {
if (count($args) + count($arguments) >= $count) {
return $rf->invokeArgs(array_merge($args, $arguments));
}
return curry($f, ...array_merge($args, $arguments));
};
}
}
namespace Article\SqlRepository
{
use Article\Article;
function getArticleById(\PDO $pdo, int $article_id): Article|null
{
$stmt = $pdo->prepare(
"SELECT article_id, name
FROM article
WHERE article_id = ?");
$stmt->execute([$article_id]);
$article = $stmt->fetchObject(Article::class);
return $article === false ? null : $article;
}
}
namespace Article\CachedArticleRepository
{
use Article\Article;
function getArticleById(callable $cacheGet, callable $cacheSet, callable $getArticleById, int $article_id): Article|null
{
$key = "article-{$article_id}";
$cached = $cacheGet($key);
if (!empty($cached)) {
return $cached;
}
$article = $getArticleById($article_id);
if ($article !== null) {
$cacheSet($key, $article);
}
return $article;
}
}
namespace {
// for this example, just reuse existing class based implementation
$runtimeCache = new RuntimeCache();
$getArticleById = curry(Article\CachedArticleRepository\getArticleById(...),
$runtimeCache->get(...), $runtimeCache->set(...),
curry(Article\SqlRepository\getArticleById(...), $pdo));
var_dump($getArticleById(1));
var_dump($getArticleById(1));
}I do things like your example here when I know at what interval the underlying data is capable of changing. Batch process runs every night at midnight, no big deal, kill and rebuild the cache when it finishes. If you know when the data can change you can rebuild the cache on demand or let it gradually repopulate on its own nicely.
The nasty part of caching is knowing when something changed in the underlying database such that the now invalidated cache entries can be evicted. Seems to me that when it absolutely needs to be up to the second kind of correct, we're best off skipping the "efficiency gain" a cache MAY offer in favor of direct SQL to an actual database connection.
You can spend money on a BIG HOG of a database one time and know precisely how much you will spend to get the speed and reliability your use case demands. If you start trying to solve this with the caching/ORM route you're expense is NOT fixed. Dev Hours vs. Hardware Cost - Im buying hardware almost every time!
I find it usually better to have short timed cache in front of the renderer instead, like caching the HTML or JSON output of a view, then you don't end up with inconsistent data because one table was cached and another wasn't.
If I take the time to flesh out a project and either have a real DBA or at least put on my DBA hat for a day to come up with a proper set of SQL functions, views, etc. that expose everything nicely. That takes quite a while, but the results are good.
ORMs like SQLAlchemy (for Python) or RedBeanPHP (for PHP) can save a lot of time when making MVPs or just gluing together a couple of open source widgets. For these "quick hack" style things I really enjoy RedBeanPHP's fluid schema:
$bean = R::dispense('article');
$bean->title = "Foo";
$bean->body = "Bar! Bar!";
$bean->datePublished = R::isoDateTime();
R::store($bean);
That little snippet of code will automatically create a table `article` and the coorisponding columns `title`, `body`, and `date_published` all with their correct types (and type promotion if needed). If I later decide to add something new to articles I just do this: $bean->whatDoesAiThink = $api->infer($bean->body);
This would automatically add the column `what_does_ai_think` to the schema.All the automatic schema stuff can be disabled (ie. in production) via `R::freeze(true)`
Other languages typically only have one dominant web framework, like Java Spring, which typically leads to a stagnating community.
A single file API is not a bad idea, but this is a bad implementation.
At least types are enforced at runtime. Wait until you learn about TypeScript.
https://github.com/hparadiz/technexus/blob/release/src/Contr...
Strict types make things easier tbh.
Any particular reason other than uhhhh... 4 spaces? This doesn't seem particularly Python-esque to me either.
Altogether it just gave me a clear feeling of this isn't a person who has written a lot of modern PHP but has probably written a lot of python.
The reality is this is a fun toy, and great job to the developer to making it work, but presenting it as anything but that is silly. There is very little benefit to using this over individual functional php files, and certainly more foot-guns.
https://github.com/prog-re/ugly/blob/main/docs/manual.md
Edit: I added links to Oink and Zaap from my readme.md. We could make a Single file PHP api framework webring!
E_ERROR is a fatal error thus it will throw (I think since PHP 7) an Error exception that can be catched with a normal try/catch and thus possible to return a proper response.
Appart from that, it's a nice project, but would benefit from better examples that actually needs an API to do things asynchronously, like a web app that needs to be able to synchronize with a server from time to time. Maybe a TODO list to keep the example short? Because the blog and gallery examples are not really convincing: why would we depend on JS to render the page and almost do the job twice while the rendering could have easily be done in PHP directly?
I understand the one file thing, but why no docs or more important any tests or static testing.
https://github.com/jcarlosroldan/oink/blob/main/documentatio...
But I get your point.
I wasn't at all saying that how it works or how to use it is not clear. Also why would you be so aggressive? In addition to missing the point, your answer is worse than "RTFM".
> This library prioritizes development speed and simplicity over everything else, including best practices and standards.
Oh. At least we were warned!
> Although any valid PHP code can be contained within a namespace, only four types of code are affected by namespaces: classes, interfaces, functions and constants.
That's why Symfony rose above, and why Laravel is taking over. Laravel is essentially "let's do a code igniter 2 but on top of symfony clean design this time".
But if I had to make one comment, after looking at the code, that serve function with that huge try/catch block is not the nicest thing to see, I would split it up and try to get some more meaningful exception and error handling.
Allows me to have an API with multiple methods and endpoints up and running in minutes. :)
As a long time php developer the repository feels like the dark times. I could understand the rant.
To get the same functionality without the extra step, simply use PostgREST [1]