64 comments

[ 4.7 ms ] story [ 178 ms ] thread
Neat! Any plans to support OpenAPI schema? It's cool that it's easy to create the API, but consuming and documenting it is also a factor.
Oh, now that would be a neat feature to have in the library
Thanks! I considered it, but since parameters are defined just because users call a validation function, there's no straighforward way to get them. Maybe I could use PHP8 attributes for this, but maybe it complicates the syntax too much for a microframework.
The title is quite confusing on what the project is. It should be "Oink: a single-file PHP library to easily build API" or something like that.

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?

Yep, more example would be useful to have, don't have to be pretty, just functional.
Or you could read the source code. It's only 178 lines of PHP.
I hope this is in jest.
Why not? There are other people in the comments who at least scanned the code. You probably wouldn't be looking at the code if we were talking about a heavier framework, such as Laravel. That 178 lines feels dense though.
I was saying the project needs more convincing examples of usage, that is, examples that makes it shine, which the current examples do not.

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

Nice! Here is my own take on the same idea (single file PHP api)

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!

A side note: I think you guys are too depreciating. "A pig in the mud", "ugly"... In the old days, we called it KISS. If you really do something improper, e.g. insecure or introducing a performance penalty, you should fix it, but otherwise I see no need of using depreciating names.
(comment deleted)
I just honestly hope this doesn't turn into another rant against PHP, as it usually happens.
Personally we have the dark times from php (v4-v7) and the good times (v7+).

As a long time php developer the repository feels like the dark times. I could understand the rant.

Please god no. People might think this is what PHP looks like in real life while it's far from the good practices that are normally used in PHP since ages.
After 15 years of PHP this is the first and only time I’ve ever seen someone actually use the “global” keyword. And to hijack $data, of all the variables you could choose…

> This library prioritizes development speed and simplicity over everything else, including best practices and standards.

Oh. At least we were warned!

Especially since it makes zero sense to do that. If you don't care about niceties and want to have a global variable without the absolute php-3 era failure prone jank just use a class static or hell even a function static or whatever. There is no reason whatsoever to use a global variable like that, hasn't been since ... decades now ? And then to name it data on top of it.
You are right, but for a project like this, with a name like this, it's rather artful and hacky. Nothing wrong in breaking the rules if you know what you are doing.
What's the significance of $data in PHP?
Nothing special, just it's hardly a rare name for a variable. And this sets it globally.
...inside a namespace called Oink
From the docs:

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

WordPress is the most relevant of all php projects and uses global. In 15 years I would have expected a php developer to at least have a look into the code base of the biggest project of that language...
The Java EE-like OOP mess of later years is no better.
It was sort of necessary for PHP renaissance, to clean up the act, but it's also "not PHP" as-in why bother using PHP if you're going to do it that way, I feel like. The whole joy of PHP is the magic of it just works coupled with a super short path from prototype to prod, but before the component-model that's now the norm and the language clean up it meant a lot of crap foundations. Wordpress is still here to attest to that.

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

(comment deleted)
That's a nice little project that I would use to quickly build an API for testing or developing an app, you just need the php executable to start it up.

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.

I love PHP but I hate the code in this repo.
I dislike PHP but quite like the repo. It's very emblematic of what I think PHP is suitable for: Quick dirty prototyping.
You can do quick prototyping without dirt in any language, it just turns out that you like working in dirt.
Honestly if I'm doing quick dirty prototyping I want to have as many tools provided for me as possible. I probably won't use most of them, but spinning up a quick Laravel project gives me built-in shortcuts for stuff that's just annoying to do in plain PHP. Sure there are conventions that say you should separate your code into different files, but if you want to you can just shove all of your code into the route files and ignore everything else.
That's a gem! It reminds me why I used to love PHP: rapid prototyping, functional style. It's a shame that I don't use it for a long time.
Personally I believe that you should always use require (require_once) instead of include (include_once) to be sure that a failure to load file creates an E_ERROR instead of an E_WARNING, this both for loading the framework but also when the framework when loading the api file.

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.

That's actually a great suggestion, I just made it into the library.
I think this is the reason why PHP is still so popular. Even with the latest 8.3 version these things still work - even though everyone else has moved to frameworks like laravel or fully OO code.

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

Native SQL is the way to go, I always push for SQL over ORM in every project I'm in.
Me too! There are cases where an ORM is nice and especially if theres a cache layer in there for rarely-changing data requests (think big retail inventory system here). But most of the time we're not doing that and regular SQL is the way to go.

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.

I wouldn't pick an ORM even for caching, ORMs are typically a multi-layered blackbox magic based God class, thus very hard to debug when things hit the fan.

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;

  ...
"I wouldn't pick an ORM even for caching, ORMs are typically a multi-layered blackbox magic based God class, thus very hard to debug when things hit the fan." -- THATS THE TRUTH! Im still scarred from various production issues over the years where the cache keeps filling up all the memory and crashing for no discernable good reason. Yeah - theres a stack trace - it says that 3rd party rats nest library code I have no control over failed... Great, Thanks! ( Hey boss! I fixed that cache bug - I changed the 3rd party code in our dependency - now we're upgrade locked forever!!! Great right!! )

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!

Yes, the paradox with ORMs are that you actually need caching because they are typically very slow, with raw SQL and good indexes you can usually query database live for most of it.

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.

I have done both (basic) approaches and I find benefits in each.

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

Yes, and I view this a strength of PHP, that you can write your own mini framework with a few lines. And as result we have many reliable frameworks to pick from for PHP.

Other languages typically only have one dominant web framework, like Java Spring, which typically leads to a stagnating community.

Reading the code, this feels like a python developer writing PHP and frankly doing a bad job. Even the formatting seems to give up readability for line count for some unknown reason. There is no typing, no following of PSR-2 in anyway, and things which make my static analysis tools very unhappy.

A single file API is not a bad idea, but this is a bad implementation.

It's a PHP project. It shouldn't have typing, that's for playing make-belief of writing in Java for the framework-oo-enterprise world. Psr-2 is often not followed, even in really good projects. And the static analysis tools are of limited use for a small code base (though to use them and make using them easier would be a plus).
> It's a PHP project. It shouldn't have typing, that's for playing make-belief of writing in Java for the framework-oo-enterprise world.

At least types are enforced at runtime. Wait until you learn about TypeScript.

Yes it definitely should have types, as is the standard in the community nowadays. Types are definitely not make-belief, as they are checked during runtime, making them actual, real types. I'd say any real project (not a prototype) that has more than 1 person working on it should definitely make use of types so that everyone would know what the shape of the expected data is.
Types are not make-belief, I know that :) I said using them in a php project is playing make-belief, it's a joke about php developers coding enterprise Java style (for php salaries ofc). Had nothing to do with how the language handles types when they are used.
That's true, modern PHP is eerily similar to Java, though the enterprise-ness of it is just an indication of not-great developers overcomplicating things (both in Java and PHP). As far as salaries go though, there's some very low ones (WordPress etc) and some very high ones, like Java senior level salaries as well, but I guess Java itself doesn't have so very low ones, which brings the average Java salary much higher than PHP.
Author here. I haven't made up my mind yet about whether types speed up or down the development in small projects, and I don't know if it defeats its purpose a bit. This is actually one of the open topics in the direction of oink.php: https://github.com/jcarlosroldan/oink/issues/3
Fwiw, please don't take my comment as a strong opinions against types in such a project. I just try not to judge projects negatively that decide not to use them, and I have a personal aversion to enterprisy code (whatever it is exactly), especially when it's heralded as the only true way, and even more especially if it overcomplicates formerly simple ecosystems, like what happened to parts of PHP.
> this feels like a python developer writing PHP

Any particular reason other than uhhhh... 4 spaces? This doesn't seem particularly Python-esque to me either.

The naming styles, the structure of the logic, the single line if statements with ternary returns, the lack of "use", the globals.

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.

Probably you consider "modern PhP" is just OOP, right ?
No, I don't. I consider modern PHP being fully typed in all places possible, tests, clear and readable code, avoid globals, and avoiding clever tricks like reflection to extract function names to autowire up routes.

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.

Seems you don't like or understand the functional programming. Being OOP is very odd to think otherwise. I know how it feels now, after 5 years of programming in Elixir. I've been there...
This is cool but, I can make an API for PHP in zero file. It would be simpler and cleaner.
I've never seen `use function Namespace\{functionName}` before, is that autoloadable with PSR-4?
No, functions are never autoloadable in PHP (except with nasty hacks).