Tools For Better Developers

News: 2021 Nov 22 - Dec 03

2021 December News

5 months ago ∙ 3 minutes read

For the last two weeks, I figured out two core features that will allow Osm Admin to stand out from other CRUD applications:

  1. It will allow mass editing of data objects.
  2. It will automatically propagate data changes throughout the database.

The foundation for the mass editing feature is data queries. Data propagation will be ensured via indexing.

I've been working on the Osm Admin indexing engine, and it's still in progress. While working on it, I reworked the inner working of data queries. Finally, I've achieved a very clear separation of various Osm Admin concerns, and documented it.

In Osm Core, I added reflection over class methods.

I've already shared various ups and downs of these efforts on Twitter in great detail, so if you are reading this, consider following me on Twitter and getting daily updates.

More details:

Osm Admin v0.1.2



In order to SELECT, INSERT, UPDATE or DELETE data objects, use query() function:

$products = query(Product::class)
    ->equals('in_stock', true)
    ->or(fn(Formula\Operation $q) => $q
        ->greater('price', 5.0)
    ->orderBy('title', desc: true)
    ->first('id', 'title', 'category.title');

$id = query(Product::class)->insert([
    'sku' => 'P123',
    'title' => 'Osm Admin',
    'price' => 0.0,
    'in_stock' => true,    

    ->greater('price', 5.0)
    ->update(['in_stock' => true]);

    ->equals('in_stock', false)

In addition to executing an SQL statement, a query:

  • validates data;
  • converts data objects to/from database records;
  • automatically joins related tables requested using dot syntax, for example category.title;
  • notifies the indexing engine about the changes.

Queries are already partly implemented. Some query features, such as validation, are yet to be implemented. Some other features, such as equals() and other filtering methods, are likely to change in the future. Yet, I'm quite happy with the foundation that's been laid down during this iteration.

Read more:


Queries automatically join related table by calling matching join method defined in the data class. For example, category.title from the above example internally calls the join_category() method:

class Product extends Object_
    public function join_category(TableQuery $query, string $joinMethod,
        string $from, string $as): void
        $query->db_query->$joinMethod("categories AS {$as}",
            "{$from}.category_id", '=', "{$as}.id");

Let me explain how it works. The query while processing category.title formula infers that the category title should be selected from a related table, and it calls the join method specifying the type of join (join or leftJoin), the alias of the main selected table (this), and the alias of the joined table (category);

$product->join_category($this, 'leftJoin', 'this', 'category');

It results in the following SQL:

SELECT category.title
FROM products AS this
LEFT OUTER JOIN categories AS category
    ON this.category_id =

Using the dot syntax, you may a distant related table. For example, category.parent.title would retrieve the title of the category that is parent to the product's category.

Joins are fully implemented.


Indexing will propagate changes by running indexers - classes that extend the base Indexer class:

#[To('scopes'), From('scopes', name: 'parent')]
class ScopeIndexer extends TableIndexer
    protected function index_level(?int $parent__level): int {
        return $parent__level ? $parent__level + 1 : 0;

    protected function index_id_path(?string $parent__id_path, int $id): string {
        return $parent__id_path ? "{$parent__id_path}/{$id}" : "{$id}";

    public function index(bool $incremental = true): void {
        // SELECT data from source tables, and INSERT/UPDATE the target table        

Indexers will incrementally process changed data. For this purpose, every INSERT/UPDATE/DELETE operation will notify all dependent indexers via change notification tables.

I hope to finish indexing in the next iteration.

Read more:

Osm Core v0.10.14


Method Reflection

From now on, you can reflect over class methods. For example:

global $osm_app; /* @var App $osm_app */
$class = $osm->app->classes[Product::class];
$method = $class->methods['join_category'];

foreach ($method->parameters as $parameter) {

Conditional Serialization

There are properties that should be sometimes serialized, and sometimes not. For example, consider Scope class. If you query title and parent.title, then the parent property should be serialized:

    "title": "Child scope",
    "parent": {
        "title": "Parent scope"

However, if you decide to store all scopes in a tree-like model, then parent property should not be serialized:

    "title": "Parent scope",
    "children": [
            "title": "Child scope"
            // not parent property

For such properties, use not_having conditional serialization:

 * @property Scope $parent #[Serialized(not_having: 'children')]
 * @property Scope[] $children #[Serialized]
class Scope extends Object_ {
} Website v0.4.4


New Content

I wrote a lot about implementing queries and indexing in Osm Admin: