The Enobrev\ORM library is a small framework of classes meant to be used for simply mapping a mysql database to PHP classes, and for creating simply SQL statements using those classes.

Related tags

Database php-orm
Overview

enobrev/php-orm

The Enobrev\ORM library is a small framework of classes meant to be used for simply mapping a mysql database to PHP classes, and for creating simply SQL statements using those classes. There's a script for parsing a mysql database and creating a file called sql.json which can be easily used by the library and other tools to understand the structure of the database, as well as a script for generating ORM class files that represent each table. The Enobrev\API library is built to interact closely and easily with this library to allow for an immediate REST-ish API frontend to your database.

Installation

composer.phar require enobrev/orm

sql_to_json

This script generates a file called sql.json, which holds an easy-to-query cache of the mysql database structure. Other tools like php-orm’s generate_tables and php-api’s generate_data_map utilize sql.json. This script should be run every time you make a change to the mysql database.

When calling this from your project, you can call it as such:

php vendor/bin/sql_to_json.php -h [host] -u [user] -d [database name] -p

The -p at the end will ask you for your database password. Once you’ve entered it, the script will ask you which tables are Many-To-Many tables, and then create a file called .sql.m2m.json to cache your response. This file merely holds a JSON array of the tables you’ve stated to be Many-To-Many tables. If, in the future, you add more m2m tables, remove that file and re-run the script.

After setting which tables are M2M, sql.json will be created or updated in the same path from which you called the script.

generate_tables

Since we have all this useful information stored in SQL.json, we may as well use it to our benefit. This script will generate two classes per chosen table, one named after the table in singular form, and the second in plural.

The singular form is meant to represent a single record in your database. If you get a record from the database, using this class, it will be returned as an instance of that class. So a Table\Car::getById($sCarId) will return an instance of Table\Car.

The plural form is meant to represent multiple records in your database. Table\Cars::get() would return an array containing multiple instances of Table\Car.

You can, of course, hand-type these classes. They simply extend Enobrev\ORM\Table and Enobrev\ORM\Tables, but they also hold database specific information, such as column names, and methods you’d like to call upon the data held within or to generate said data (like for UUIDs). Most of this can be considered boilerplate, if at least as a starting point, and can be generated using the generate_tables script.

From your project root, after generating sql.json as explained in the previous section, you can call to generate table classes for your project.

php vendor/bin/generate_tables.php -j ./sql.json -n [namespace] -o [output path]

An example:

php vendor/bin/generate_tables.php -j ./sql.json -n Enobrev\\Table -o lib/Table

This will generate tables in the path lib/Table with the PHP Namespace of Enobrev\Table, presumably for a project with its own namespace of Enobrev.

Once you enter this command, it will explicitly list all the available tables and ask you to enumerate which table classes you’d like to generate.

Keep in mind that it will simply generate the files and overwrite whatever is there. So if you’ve modified a previously generated class file, be sure to check it into your source control before running this script so you can diff it afterwards.

SQLBuilder and SQL

Once you’ve generated your class files, you’ll want to general SQL statements using them. This is what the Enobrev\php-orm library is all about. The library itself is made up of multiple classes meant to be tied together to generate a proper SQL statement. All of the functionality in these classes has been encapsulated into two classes that are meant to be used in tandem: SQLBuilder and SQL

Overall, this library is meant for simple queries and simple LEFT OUTER JOINs . Anything requiring more advanced SQL functionality, including SQL methods is beyond the scope of this library classes and should instead be written out by hand. That said, it’s still possible to use the SQL class to help with some of your hand-written SQL.

For plain string SQL, you can simply call Db::getInstance()->query(``"``SELECT * FROM cars``"``);, which will return a PDOResult instance which you can use in the normal way. Db::query also accepts instances of SQL and SQLBuilder.

These classes are very simple and are meant to be used together. For instance, it’s not possible to generate a large tree of conditions in SQLBuilder without using the SQL class. In essence, these two classes are a UI built on top of the inner-workings of the Enobrev\ORM library.

For an example usage, let’s create a couple table classes to play with. Since the primary purpose of this library is to generate SQL, this code can work just fine without an actual database.

        require_once __DIR__ .'/../vendor/autoload.php';

        use DateTime;
        use Enobrev\ORM\Field as Field;
        use Enobrev\ORM\Table;
        use Enobrev\SQL;

        class User extends Table {
            protected $sTitle = 'users';

            /** @var  Field\Id */
            public $user_id;

            /** @var  Field\Text */
            public $user_name;

            /** @var  Field\Text */
            public $user_email;

            /** @var  Field\DateTime */
            public $user_date_added;

            /** @var  Field\Boolean */
            public $happy;

            protected function init() {
                $this->addPrimaries(
                    new Field\Id('user_id')
                );

                $this->addFields(
                    new Field\Text('user_name'),
                    new Field\Text('user_email'),
                    new Field\DateTime('user_date_added'),
                    new Field\Boolean('happy')
                );

                $this->happy->setDefault(false);
            }
        }

        class Address extends Table {
            protected $sTitle = 'addresses';

            /** @var  Field\Id */
            public $address_id;

            /** @var  Field\Id */
            public $user_id;

            /** @var  Field\Text */
            public $address_1;

            /** @var  Field\Text */
            public $address_city;

            protected function init() {
                $this->addPrimaries(
                    new Field\Id('address_id')
                );

                $this->addFields(
                    new Field\Id('user_id'),
                    new Field\Text('address_1'),
                    new Field\Text('address_city')
                );
            }
        }

Here’s how to generate a SQL query using the SQL class:

        $oUser = new User();
        $oSQL  = SQL::select(
            $oUser,
            $oUser->user_id,
            $oUser->user_name,
            $oUser->user_email,
            Address::Field('address_city', 'billing'),
            Address::Field('address_city', 'shipping'),
            SQL::join($oUser->user_id, Address::Field('user_id', 'billing')),
            SQL::join($oUser->user_id, Address::Field('user_id', 'shipping')),
            SQL::either(
                SQL::also(
                    SQL::eq($oUser->user_id, 1),
                    SQL::eq($oUser->user_email, '[email protected]')
                ),
                SQL::between($oUser->user_date_added, new DateTime('2015-01-01'), new DateTime('2015-06-01'))
            ),
            SQL::asc($oUser->user_name),
            SQL::desc($oUser->user_email),
            SQL::group($oUser->user_id),
            SQL::limit(5)
        );

        echo (string) $oSQL;

Here’s that same example using SQLBuilder

        $oUser = new User();

        $oSQL = SQLBuilder::select($oUser);
        $oSQL->fields(
            $oUser->user_id,
            $oUser->user_name,
            $oUser->user_email,
            Address::Field('address_city', 'billing'),
            Address::Field('address_city', 'shipping'),
        );
        $oSQL->join($oUser->user_id, Address::Field('user_id', 'billing'));
        $oSQL->join($oUser->user_id, Address::Field('user_id', 'shipping'));
        $oSQL->either(
                SQL::also(
                    SQL::eq($oUser->user_id, 1),
                    SQL::eq($oUser->user_email, '[email protected]')
                ),
                SQL::between($oUser->user_date_added, new DateTime('2015-01-01'), new DateTime('2015-06-01'))
        );

        // SQLBuilder returns an instance of itself, so you can also string calls like so:
        $oSQL->asc($oUser->user_name)->desc($oUser->user_email)->group($oUser->user_id)
        $oSQL->limit(5);

        echo (string) $oSQL;

The order of all these method calls doesn’t matter.

The primary difference between SQL and SQLBuilder, besides the interface, is that SQL will return Enobrev\ORM objects, while SQLBuilder returns an instance of itself. Those objects can be used outside of the realm of creating one full SQL string.

For instance:

    $oCondition = SQL::also(
        SQL::eq($oUser->user_id, 1),
        SQL::eq($oUser->user_email, '[email protected]')
    );

    // $oCondition now has an instance of Enobrev\ORM\Condition
    echo $oCondition->toSQL();
    // echos: users.user_id = 1 AND users.user_email = '[email protected]`

    echo SQL::limit(5, 10)->toSQL(); // LIMIT 5, 10
    echo SQL::select($oUsers); // SELECT * FROM users

    $oUser = new User();
    $oUser->user_id   = 1;
    $oUser->user_name = 'Mark';
    echo SQL::update($oUser); // UPDATE users SET user_name = 'Mark' WHERE user_id = 1
    echo SQL::insert($oUser); // INSERT INTO users (user_id, user_name) VALUES (1, 'Mark')
Comments
  • Bump twig/twig from 3.4.1 to 3.4.3

    Bump twig/twig from 3.4.1 to 3.4.3

    Bumps twig/twig from 3.4.1 to 3.4.3.

    Changelog

    Sourced from twig/twig's changelog.

    3.4.3 (2022-09-28)

    • Fix a security issue on filesystem loader (possibility to load a template outside a configured directory)

    3.4.2 (2022-08-12)

    • Allow inherited magic method to still run with calling class
    • Fix CallExpression::reflectCallable() throwing TypeError
    • Fix typo in naming (currency_code)
    Commits
    • c38fd6b Prepare the 3.4.3 release
    • 5a858ac Merge branch '2.x' into 3.x
    • ab40267 Prepare the 2.15.3 release
    • fc18c2e Update CHANGELOG
    • 2e8acd9 Merge branch '2.x' into 3.x
    • d6ea14a Merge branch '1.x' into 2.x
    • 35f3035 security #cve- Fix a security issue on filesystem loader (possibility to load...
    • be33323 Merge branch '2.x' into 3.x
    • 9170edf Fix doc CS
    • fab3e0f minor #3744 Adding installation instructions for Symfony (ThomasLandauer)
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 1
  • Bump laminas/laminas-diactoros from 2.9.0 to 2.13.0

    Bump laminas/laminas-diactoros from 2.9.0 to 2.13.0

    Bumps laminas/laminas-diactoros from 2.9.0 to 2.13.0.

    Release notes

    Sourced from laminas/laminas-diactoros's releases.

    2.13.0

    Release Notes for 2.13.0

    Feature release (minor)

    2.13.0

    • Total issues resolved: 0
    • Total pull requests resolved: 4
    • Total contributors: 3

    Enhancement

    renovate

    2.12.0

    Release Notes for 2.12.0

    Feature release (minor)

    2.12.0

    • Total issues resolved: 0
    • Total pull requests resolved: 5
    • Total contributors: 4

    Bug

    Enhancement

    Documentation,Enhancement

    2.11.3

    Release Notes for 2.11.3

    2.11.x bugfix release (patch)

    ... (truncated)

    Commits
    • 34ba650 Merge pull request #106 from Ocramius/feature/laminas-coding-standard-2.3.x-t...
    • 7880719 Improved type signature for parse_cookie_header() return type
    • 2539f32 Refined types as per laminas/laminas-coding-standard:2.3.x upgrades
    • 739ad4d Merge pull request #103 from gsteel/update-laminas-coding-standard
    • 98f1d99 Change set/restore error handler to a try/catch block
    • 091cd93 Bump PHPUnit to ^9.5
    • 4f7b49b Removes the GMP case from the data provider
    • defeaf5 Bump interop integration tests to fix test failures on lowest
    • 1341d78 Baseline newly discovered psalm issues in src/
    • cd02372 qa: apply CS fixes
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 1
  • had a problem importing date times

    had a problem importing date times

    Due to a formatting issue in the way sql times were being initialized (with Dates). It's trying to insert an sql time column with a Y-m-d string https://github.com/enobrev/php-orm/blob/master/lib/ORM/Field/Date.php#L69

    I was able to get it to run locally by swapping the orm Date parent with a DateTime parent https://github.com/enobrev/php-orm/blob/master/lib/ORM/Field/Time.php#L8

    class Time extends Date { -> class Time extends DateTime
    

    I think that was just a lucky conversion, here's the original issue:

    Fatal error: Uncaught Enobrev\ORM\DbException: SQLSTATE[22007]: Invalid datetime format: 1292 Incorrect time value: '2018-05-11' for column 'place_hour_open' at row 1 in SQL: INSERT INTO place_hours ( place_hour_id, place_id, place_hour_day, place_hour_open, place_hour_close, place_hour_24, place_hour_timezone ) VALUES ( NULL, '53ab744f498e0e151c757af0', '7', '2018-05-11', '2018-05-11', '0', 'UTC' ) in /Users/messel/Desktop/Dropbox/code/welcome/api.welco.me/vendor/enobrev/php-orm/lib/ORM/Db.php:229

    The expected place_hour_open and place_hour_close should be sql time columns but look like date strings: '2018-05-11', '2018-05-11',

    opened by victusfate 1
  • Bump twig/twig from 3.3.2 to 3.3.8

    Bump twig/twig from 3.3.2 to 3.3.8

    Bumps twig/twig from 3.3.2 to 3.3.8.

    Changelog

    Sourced from twig/twig's changelog.

    3.3.8 (2022-02-04)

    • Fix a security issue when in a sandbox: the sort filter must require a Closure for the arrow parameter
    • Fix deprecation notice on round
    • Fix call to deprecated convertToHtml method

    3.3.7 (2022-01-03)

    • Allow more null support when Twig expects a string (for better 8.1 support)
    • Only use Commonmark extensions if markdown enabled

    3.3.6 (2022-01-03)

    • Only use Commonmark extensions if markdown enabled

    3.3.5 (2022-01-03)

    • Allow CommonMark extensions to easily be added
    • Allow null when Twig expects a string (for better 8.1 support)
    • Make some performance optimizations
    • Allow Symfony translation contract v3+

    3.3.4 (2021-11-25)

    • Bump minimum supported Symfony component versions
    • Fix a deprecated message

    3.3.3 (2021-09-17)

    • Allow Symfony 6
    • Improve compatibility with PHP 8.1
    • Explicitly specify the encoding for mb_ord in JS escaper
    Commits
    • 972d860 Prepare the 3.3.8 release
    • b265233 Merge branch '2.x' into 3.x
    • fca80b5 Bump version
    • 66baa66 Prepare the 2.14.11 release
    • 9e5ca74 Merge branch '2.x' into 3.x
    • 22b9dc3 bug #3641 Disallow non closures in sort filter when the sanbox mode is enab...
    • 2eb3308 Disallow non closures in sort filter when the sanbox mode is enabled
    • 25d410b Merge branch '2.x' into 3.x
    • e056e63 bug #3638 Fix call to deprecated "convertToHtml" method (jderusse)
    • 779fdd0 Fix call to deprecated "convertToHtml" method
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Feature: Bulk Inserts

    Feature: Bulk Inserts

    Would be ideal to be able to build a query for massive bulk inserts for more efficient imports

    Queries like

    INSERT IGNORE INTO some_table (field_1, field_2) VALUES
    (1, 2),
    (3, 4),
    (5, 6),
    (7, 8)
    
    enhancement 
    opened by enobrev 0
Owner
Mark Armendariz
Freelance Developer and Co-Founder at Welcome. You can "see" my work at welco.me, vimeo.com/cameo, freemusicarchive.com, and on-camera-audiences.com
Mark Armendariz
Staggered import of large and very large MySQL Dumps even through the web servers with hard runtime limit and those in safe mode.

Staggered import of large and very large MySQL Dumps (like phpMyAdmin dumps) even through the web servers with hard runtime limit and those in safe mode. | Persian Translation Version

Amir Shokri 5 Jan 8, 2022
Small script for importing the KvK (Dutch Chamber of Commerce) Open Data Set (CSV file) to a MySQL database.

KvK-CSV-2-SQL Small script for importing the KvK (Dutch Chamber of Commerce) Open Data Set (CSV file) to a MySQL database. Table of content KvK-CSV-2-

BASTIAAN 3 Aug 5, 2022
Propel2 is an open-source high-performance Object-Relational Mapping (ORM) for modern PHP

Propel2 Propel2 is an open-source Object-Relational Mapping (ORM) for PHP. Requirements Propel uses the following Symfony Components: Config Console F

Propel 1.2k Dec 27, 2022
Doctrine PHP mapping driver

WORK IN PROGRESS! Doctrine PHP mapping driver Alternative mapping driver that allows to write mappings in PHP. Documentation Associations examples TOD

Andrey Klimenko 3 Aug 15, 2021
SleekwareDB is a NoSQL database storage service. A database storage service that can be used for various platforms and is easy to integrate.

SleekwareDB is a NoSQL database storage service. A database storage service that can be used for various platforms and is easy to integrate. NoSQL API

SleekwareDB 12 Dec 11, 2022
Tiny php mysql lib (PDO-based) with handy fetch/update functionality, supports both SQL and parametric queries

Micro PHP mysql lib (~ 200 lines of code) with ultra powerful CRUD for faster than ever development: parametric fetch/insert/update/delete (based on a

Mr Crypster 18 Dec 10, 2022
Independent query builders for MySQL, PostgreSQL, SQLite, and Microsoft SQL Server.

Aura.SqlQuery Provides query builders for MySQL, Postgres, SQLite, and Microsoft SQL Server. These builders are independent of any particular database

Aura for PHP 424 Dec 12, 2022
A validating SQL lexer and parser with a focus on MySQL dialect.

SQL Parser A validating SQL lexer and parser with a focus on MySQL dialect. Code status Installation Please use Composer to install: composer require

phpMyAdmin 368 Dec 27, 2022
API abstracting communication with SQL providers (eg: MySQL) on top of PDO inspired by Java JDBC

SQL Data Access API Table of contents: About Configuration Execution Installation Unit Tests Examples Reference Guide About This API is a ultra light

Lucian Gabriel Popescu 0 Jan 9, 2022
SQL database access through PDO.

Aura.Sql Provides an extension to the native PDO along with a profiler and connection locator. Because ExtendedPdo is an extension of the native PDO,

Aura for PHP 533 Dec 30, 2022
A php securised login system, using Hash, Salt and prevent from SQL Injections

A Basic Secure Login Implementation Hashed & Salted password ( only hashed in ShA-512 for now ) No SQL injection possible Prevent XSS attacks from the

Yohann Boniface 1 Mar 6, 2022
ATK Data - Data Access Framework for high-latency databases (Cloud SQL/NoSQL).

ATK Data - Data Model Abstraction for Agile Toolkit Agile Toolkit is a Low Code framework written in PHP. Agile UI implement server side rendering eng

Agile Toolkit 257 Dec 29, 2022
Connect and work with MySQL/MariaDB database through MySQLi in PHP. This is an introductory project, If you need a simple and straightforward example that takes you straight to the point, you can check out these examples.

First MySQLi PHP Connect and work with MySQL/MariaDB database through MySQLi in PHP. The above exercises are designed for students. This is an introdu

Max Base 4 Feb 22, 2022
TO DO LIST WITH LOGIN AND SIGN UP and LOGOUT using PHP and MySQL please do edit the _dbconnect.php before viewing the website.

TO-DO-LIST-WITH-LOGIN-AND-SIGN-UP TO DO LIST WITH LOGIN AND SIGN UP and LOGOUT using PHP and MySQL please do edit the _dbconnect.php before viewing th

Aniket Singh 2 Sep 28, 2021
A Laravel package to output a specific sql to your favourite debugging tool. The supported log output is Laravel Telescope, Laravel Log, Ray, Clockwork, Laravel Debugbar and your browser.

Laravel showsql A Laravel package to output a specific sql to your favourite debugging tool, your browser or your log file. Use case You often want to

Dieter Coopman 196 Dec 28, 2022
A mysql database backup package for laravel

Laravel Database Backup Package This package will take backup your mysql database automatically via cron job. Installing laravel-backup The recommende

Mahedi Hasan Durjoy 20 Jun 23, 2021
Laravel Code Generator based on MySQL Database

Laravel Code Generator Do you have a well structed database and you want to make a Laravel Application on top of it. By using this tools you can gener

Tuhin Bepari 311 Dec 28, 2022
High performance distributed database for mysql

High performance distributed database for mysql, define shardings by velocity&groovy scripts, can be expanded nodes flexible...

brucexx 33 Dec 13, 2022
Async MySQL database client for ReactPHP.

MySQL Async MySQL database client for ReactPHP. This is a MySQL database driver for ReactPHP. It implements the MySQL protocol and allows you to acces

Friends of ReactPHP 302 Dec 11, 2022