<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: giuliopanda</title>
    <description>The latest articles on Forem by giuliopanda (@giuliopanda).</description>
    <link>https://forem.com/giuliopanda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3287278%2Fcfd4fc05-f551-44c4-b282-13ed81b95d7e.png</url>
      <title>Forem: giuliopanda</title>
      <link>https://forem.com/giuliopanda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/giuliopanda"/>
    <language>en</language>
    <item>
      <title>EAV: a fascinating, but often dangerous pattern</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Tue, 10 Mar 2026 16:29:53 +0000</pubDate>
      <link>https://forem.com/giuliopanda/eav-a-fascinating-but-often-dangerous-pattern-200j</link>
      <guid>https://forem.com/giuliopanda/eav-a-fascinating-but-often-dangerous-pattern-200j</guid>
      <description>&lt;p&gt;The Entity-Attribute-Value (EAV) model is a solution that initially looks very clever.&lt;br&gt;
It allows developers to add new fields without changing the database structure.&lt;br&gt;
For this reason it has often been used in CMSs, e-commerce platforms, and systems that support dynamic fields.&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of storing data in tables with fixed columns such as name, surname, or age,&lt;br&gt;
data is saved as generic rows composed of entity, attribute, and value.&lt;br&gt;
This makes the database very flexible, but that flexibility comes with a cost.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it is still used today
&lt;/h2&gt;

&lt;p&gt;EAV is still widely used because it allows systems to support custom fields,&lt;br&gt;
add extensions without modifying the database schema,&lt;br&gt;
and keep a structure that appears stable over time.&lt;br&gt;
For these reasons it still appears in many modern projects, including those written in PHP.&lt;/p&gt;

&lt;p&gt;The problem is that what looks convenient at the beginning can become complicated as the project grows.&lt;/p&gt;
&lt;h2&gt;
  
  
  Main problems
&lt;/h2&gt;

&lt;p&gt;The first issue is query complexity.&lt;br&gt;
In a traditional relational database, reading data is simple and direct.&lt;br&gt;
With EAV, even basic operations often require multiple joins,&lt;br&gt;
data transformations, and logic that is harder to optimize.&lt;/p&gt;

&lt;p&gt;The second issue is performance.&lt;br&gt;
EAV tables tend to grow very quickly,&lt;br&gt;
indexes become less effective,&lt;br&gt;
and the database has to work harder to perform fast searches.&lt;/p&gt;

&lt;p&gt;There is also a data integrity problem.&lt;br&gt;
In a traditional relational model, the database itself can enforce types, constraints, and consistency.&lt;br&gt;
With EAV, many of these guarantees move into the application code,&lt;br&gt;
which can make the system more fragile.&lt;/p&gt;

&lt;p&gt;Finally, EAV makes the database harder to understand.&lt;br&gt;
In a relational system the structure is visible directly in the tables.&lt;br&gt;
In an EAV system the real structure is hidden inside the data itself&lt;br&gt;
and often has to be reconstructed by reading queries and application logic.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it made sense in the past
&lt;/h2&gt;

&lt;p&gt;Historically, EAV also became popular because modifying database tables used to be risky.&lt;br&gt;
Operations such as ALTER TABLE could lock large tables or require long downtime.&lt;br&gt;
To avoid these problems, many systems preferred to use a generic structure.&lt;/p&gt;

&lt;p&gt;Today, however, modern relational databases handle schema changes much better.&lt;br&gt;
In many cases adding a column is much simpler and safer than it used to be.&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The real advantage of EAV is flexibility,&lt;br&gt;
but that flexibility often comes at the cost of slower queries,&lt;br&gt;
more complex code,&lt;br&gt;
less clarity,&lt;br&gt;
and weaker data management.&lt;/p&gt;

&lt;p&gt;For many modern systems, modifying the database structure directly can be a better choice:&lt;br&gt;
clearer, faster, and easier to maintain over time.&lt;/p&gt;

&lt;p&gt;Inside MilkAdmin I decided to experiment with this idea by building an admin form system&lt;br&gt;
that, instead of using EAV, modifies the structure of the original database tables directly.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/giuliopanda" rel="noopener noreferrer"&gt;
        giuliopanda
      &lt;/a&gt; / &lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;
        milk-admin
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source PHP admin panel and dashboard built with Bootstrap 5 — relational CRUD generator, authentication, REST API, CLI, modular architecture, MySQL &amp;amp; SQLite.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Milk Admin&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/giuliopanda/repo/raw/main/milkadmin-img01.jpg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fgiuliopanda%2Frepo%2Fraw%2Fmain%2Fmilkadmin-img01.jpg" alt="Milk Admin"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/08cef40a9105b6526ca22088bc514fbfdbc9aac1ddbf8d4e6c750e3a88a44dca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://www.php.net/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/bc2a9405de1e364a76db18c6d973eb62a62408d93acfce26242cb6e0891818cb/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382532422d707572706c652e737667" alt="PHP Version"&gt;&lt;/a&gt;
&lt;a href="https://www.mysql.com/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/da383cdb78646981c9fd58061a969a5a25e15e7f53e13fc22e6421de578aecfe/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d7953514c2d737570706f727465642d626c75652e737667" alt="MySQL"&gt;&lt;/a&gt;
&lt;a href="https://www.sqlite.org/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/598c046a21467d87af6faf99187afc7b169f91902e9fb0745dd8f2898a9551ca/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53514c6974652d737570706f727465642d626c75652e737667" alt="SQLite"&gt;&lt;/a&gt;
&lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/05bf67bcc1691d21c4448fadeb910a3e528e3ad71fb8abf1cd73f5a0624e2257/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5374617475732d426574612d6f72616e67652e737667" alt="Status"&gt;&lt;/a&gt;
&lt;a href="https://github.com/giuliopanda/milk-admin/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/eb7dbd27a823d7cd8853f5e58d8f125dab76cb8164dcf9907db66a91bd15fc6b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f54657374732d504850556e69742d677265656e2e737667" alt="Tests"&gt;&lt;/a&gt;
&lt;a href="https://github.com/giuliopanda/milk-admin/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/793cf7e413cbfda088e83586070506fe3359400af15b80aeafa9f3c986bdd2ae/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d4c6576656c253230352d626c75652e737667" alt="Static Analysis"&gt;&lt;/a&gt;
&lt;a href="https://milkadmin.org/milk-admin/?page=docs&amp;amp;action=Developer/GettingStarted/introduction" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/05e427e5dfbce9804aad8bea904878024ddafe948c4e268b76052a34ac276319/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f63732d4f6e6c696e652d677265656e2e737667" alt="Documentation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milk Admin&lt;/strong&gt; is a PHP framework for building backoffice and internal management tools — without framework lock-in, SaaS dependencies, or vendor tie-ins.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Project Status: Beta&lt;/strong&gt;
The core architecture is stable. Actively developed toward a 1.0 release in mid-2026.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Official links:&lt;/strong&gt;
&lt;a href="https://www.milkadmin.org/" rel="nofollow noopener noreferrer"&gt;Website&lt;/a&gt; ·
&lt;a href="https://milkadmin.org/milk-admin/?page=docs&amp;amp;action=Developer/GettingStarted/introduction" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt; ·
&lt;a href="https://milkadmin.org/demo/?page=auth&amp;amp;action=login" rel="nofollow noopener noreferrer"&gt;Live Demo&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Form Builder That Generates Code&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Most form builders store their configuration in the database and generate forms at runtime. Milk Admin is different.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Projects module&lt;/strong&gt; lets you design modules visually — tables, forms, field types, relations — and generates real PHP modules that you own completely.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/adaa0089a199f468cbad9a4563909dee4551e54bdc34568ad41677ab181f8b06/68747470733a2f2f7777772e6d696c6b61646d696e2e6f72672f6173736574732f6172742d30392d3039352e676966"&gt;&lt;img src="https://camo.githubusercontent.com/adaa0089a199f468cbad9a4563909dee4551e54bdc34568ad41677ab181f8b06/68747470733a2f2f7777772e6d696c6b61646d696e2e6f72672f6173736574732f6172742d30392d3039352e676966" alt="Form Builder Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The builder generates PHP modules extended by JSON configuration files&lt;/li&gt;
&lt;li&gt;The JSON layer handles the builder's configuration updates&lt;/li&gt;
&lt;li&gt;The PHP code is always the final authority — modify, extend, or rewrite freely&lt;/li&gt;
&lt;li&gt;Both layers coexist: you can start with the builder and continue with code&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means you can &lt;strong&gt;prototype rapidly&lt;/strong&gt;…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




</description>
      <category>mysql</category>
      <category>php</category>
      <category>sqlite</category>
      <category>database</category>
    </item>
    <item>
      <title>An Independent backoffice for WordPress?</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Sun, 15 Feb 2026 21:51:15 +0000</pubDate>
      <link>https://forem.com/giuliopanda/an-independent-backoffice-for-wordpress-24co</link>
      <guid>https://forem.com/giuliopanda/an-independent-backoffice-for-wordpress-24co</guid>
      <description>&lt;h2&gt;
  
  
  It All Started with Metadata
&lt;/h2&gt;

&lt;p&gt;Milk Admin natively relies on two separate databases — one for internal configuration, the other for managing external data. This architecture made it a natural candidate for integration with external systems, such as a WordPress database.&lt;/p&gt;

&lt;p&gt;Once connected, I was able to start managing a WordPress site's tables directly from my panel. From there, an immediate need emerged: handling metadata relationships natively. I looked at how the most well-known and mature systems similar to mine handled this, and I found that there usually isn't a native system for it. So I wrote hasMeta, a method that handles native metadata relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it looks in a model definition
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rule&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#__users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'nickname'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserMetaModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasMeta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'city'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserMetaModel&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then there are a number of other small conventions I inherited from WordPress: the dynamic table prefix (#__) and a functions.php file for more advanced customizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Small Personal Win
&lt;/h2&gt;

&lt;p&gt;Every now and then I try to promote my work — I'm terrible at it, but I try. When I showed the metadata functionality to a client of mine, his reaction was enthusiastic. He asked me for a demo… that had nothing to do with metadata. He wanted to verify whether my admin panel could handle WordPress authentication.&lt;/p&gt;

&lt;p&gt;A different request than expected, but an exciting one, so I got to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The WordPress Plugin
&lt;/h2&gt;

&lt;p&gt;I developed a plugin that hooks into WordPress's authenticate filter, with priority 30, to intervene after the standard checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'authenticate'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'authenticate'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method receives three parameters: $user (the result of previous checks), $username, and $password. If conditions require it, the plugin makes a POST call to my admin panel's endpoint via wp_remote_post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'headers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'request_timeout'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'body'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wp_json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'username'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'password'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_remote_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Authentication Endpoint
&lt;/h2&gt;

&lt;p&gt;On the Milk Admin side, I created a dedicated module that exposes a public endpoint for credential verification:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#[ApiEndpoint('public-auth/check-credentials', 'POST')]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Reachable with a simple call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;public_html&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;
&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;application&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"username"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"password"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this demo the table only contains username and password, but nothing prevents extending the response with additional data such as roles, permissions, or profile information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key design choices
&lt;/h2&gt;

&lt;p&gt;The plugin is designed not to interfere with administrators and not to block the native flow when username or password are empty: in those cases, WordPress handles everything as usual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;What started as an experiment with metadata turned into a concrete demonstration of interoperability. My admin panel doesn't replace WordPress — it works alongside it: it makes it easy to create a management panel that extends WordPress data, but I discovered it can also integrate well via API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;Github project&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>wordpress</category>
      <category>webdev</category>
    </item>
    <item>
      <title>MilkAdmin 0.9.0 - Toward a First Stable Version</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Mon, 26 Jan 2026 10:38:42 +0000</pubDate>
      <link>https://forem.com/giuliopanda/milkadmin-090-toward-a-first-stable-version-49jp</link>
      <guid>https://forem.com/giuliopanda/milkadmin-090-toward-a-first-stable-version-49jp</guid>
      <description>&lt;p&gt;&lt;strong&gt;MilkAdmin is a lightweight PHP admin panel, designed to remain understandable even after a long time&lt;/strong&gt;. With version 0.9.0, it officially enters beta phase: it's not yet a 1.0, but it's the point where this system now has a clear, coherent shape and, most importantly, will be used in a real project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why MilkAdmin Exists
&lt;/h1&gt;

&lt;p&gt;MilkAdmin is a &lt;strong&gt;personal project&lt;/strong&gt;, born from a very practical need.&lt;/p&gt;

&lt;p&gt;I don't usually work on backends based on popular frameworks: I work on various PHP products developed for specific needs. However, I wasn't able to develop even simple backends.&lt;/p&gt;

&lt;p&gt;I tried studying a backoffice like Filament, but after abandoning it for a while, when I tried to pick it up again I realized how difficult it was to re-orient myself. I would have had to start practically from scratch.&lt;/p&gt;

&lt;p&gt;Hence the idea: &lt;strong&gt;create a system that remains understandable even after months of inactivity&lt;/strong&gt;, a project that, once reopened, allows you to immediately understand what you had done and why.&lt;/p&gt;

&lt;p&gt;At first it was just an idea, I didn't think I would ever complete it, also because it was objectively more work than studying Filament. So at the beginning I tentatively made small experiments on creating a framework, from there I started trying to understand how in my opinion a system should behave. Then over time it became a hobby, and eventually something much more mature than I expected.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Philosophy: Simplicity and Control
&lt;/h1&gt;

&lt;h3&gt;
  
  
  The main goal of MilkAdmin is to remain understandable even after a long time.
&lt;/h3&gt;

&lt;p&gt;The idea is that, when opening a module, it should be easy to understand how it's structured, what data it manages, how they are validated, how they are connected to other data, and how they are used in the backend - even if you don't remember how the framework works. However, I wanted the creation of a basic CRUD to be very concise, so I reformulated where concepts should be written.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;namespace Local\Modules\Recipe;
use App\Abstracts\AbstractModel;

class RecipeModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule-&amp;gt;table('#__recipes')
            -&amp;gt;id()
            -&amp;gt;title('name')-&amp;gt;index()
            -&amp;gt;text('ingredients')-&amp;gt;formType('textarea')
            -&amp;gt;select('difficulty', ['Easy', 'Medium', 'Hard']);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I reflected on what the use case might be: In real business contexts, data often already exists, coming from Excel, SharePoint, or legacy flows, and cannot always be reorganized according to ideal schemas. MilkAdmin deliberately stays close to PHP, without excessive abstractions, leaving full control over queries, relationships, and flows.&lt;/p&gt;

&lt;h1&gt;
  
  
  Let's Be Realistic: I Don't Expect MilkAdmin to Be Adopted by Other Projects
&lt;/h1&gt;

&lt;p&gt;An Admin Panel that doesn't use Laravel or Symfony &lt;strong&gt;doesn't attract the community's attention&lt;/strong&gt;, and I'm perfectly aware of that.&lt;/p&gt;

&lt;p&gt;I'm an "old-school" programmer: I prefer clear code over abstract code, even if it's less "elegant" by certain standards. Sometimes files are long, I still use static classes, and if the code is readable and works, that's fine by me.&lt;/p&gt;

&lt;p&gt;What I care about, instead, is open source: sharing your own work and studies to create a dialogue that doesn't necessarily need to involve large numbers, but that can be useful even to just a few people.&lt;/p&gt;

&lt;p&gt;At this point, if the project intrigues you, head over to GitHub, and if you'd like to leave a star - it would make me very happy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These reflections come from the development of MilkAdmin, a PHP admin panel system I actively maintain. Code, demos, and documentation are available at &lt;a href="https://www.milkadmin.org/" rel="noopener noreferrer"&gt;milkadmin.org&lt;/a&gt;. MIT licensed.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>backend</category>
    </item>
    <item>
      <title>When CRUD Tables Are No Longer Enough</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Fri, 09 Jan 2026 21:43:50 +0000</pubDate>
      <link>https://forem.com/giuliopanda/when-crud-tables-are-no-longer-enough-53f</link>
      <guid>https://forem.com/giuliopanda/when-crud-tables-are-no-longer-enough-53f</guid>
      <description>&lt;p&gt;Anyone who has built an admin panel in PHP knows how this story usually ends. At the beginning, everything is simple: a table, a form, a couple of filters. Then real requirements arrive — relationships between data, more complex flows, partial updates — and the classic “table + form” CRUD starts to crack.&lt;/p&gt;

&lt;p&gt;It’s not CRUD’s fault. The problem is that admin interfaces are not just CRUD.&lt;br&gt;
A real admin needs to see data in different contexts, navigate relationships without losing state, and edit related entities quickly.&lt;/p&gt;

&lt;p&gt;These reflections led me to explore different approaches, until I decided to build a system from scratch, outside of the most popular frameworks. This is where I ended up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Different Way to Think About the Problem&lt;/strong&gt;&lt;br&gt;
Instead of thinking “I need to build a page”, I started thinking “I need to describe a view of this data”.&lt;br&gt;
This led me to builders: PHP objects that describe what to display and how. Not frontend components, not magic widgets — just PHP structures that generate HTML, handle queries, and return either HTML or JSON responses.&lt;br&gt;
The code stays PHP. The flow stays request → processing → response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Concrete Example: Posts&lt;/strong&gt;&lt;br&gt;
Let’s start with the simplest case: a module to manage posts with a title and content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Model defines the data structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class PostsModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule-&amp;gt;table('#__posts')
            -&amp;gt;id()
            -&amp;gt;string('title')-&amp;gt;index()
            -&amp;gt;text('content')-&amp;gt;formType('editor');
    }

    #[Validate('title')]
    public function validateTitle($current_record_obj): string {
        $value = $current_record_obj-&amp;gt;title;
        if (strlen($value) &amp;lt; 5) {
            return 'Title must be at least 5 characters long';
        }
        return '';
    }
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is the source of truth. It declares the table, the fields, how they should appear in forms, and where validation belongs — in the model, where it actually makes sense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Controller uses builders to generate views&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class PostsController extends AbstractController
{ 
    #[RequestAction('home')]
    public function postsList() {
        $tableBuilder = TableBuilder::create($this-&amp;gt;model, 'idTablePosts')
            -&amp;gt;field('content')-&amp;gt;truncate(50)
            -&amp;gt;field('title')-&amp;gt;link('?page=posts&amp;amp;action=edit&amp;amp;id=%id%')
            -&amp;gt;setDefaultActions();

        $response = array_merge($this-&amp;gt;getCommonData(), $tableBuilder-&amp;gt;getResponse());
        Response::render(MILK_DIR . '/Theme/SharedViews/list_page.php', $response);
    }

    #[RequestAction('edit')]
    public function postEdit() {
        $response = $this-&amp;gt;getCommonData();
        $response['form'] = FormBuilder::create($this-&amp;gt;model, $this-&amp;gt;page)-&amp;gt;getForm();
        Response::render(MILK_DIR . '/Theme/SharedViews/edit_page.php', $response);
    }
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TableBuilder already knows how to handle search, pagination, and sorting. FormBuilder knows which fields to show and how to validate them. You describe what you want — the builders generate the rest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysq235qhfa5ogfxh6isw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fysq235qhfa5ogfxh6isw.gif" alt=" " width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk43ackzozhuukbc0ifv6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk43ackzozhuukbc0ifv6.gif" alt=" " width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Views are plain PHP templates — no templating engines, no special syntax. If you need to change something, you open the file and edit the HTML directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Things Get Complicated: Relationships
&lt;/h2&gt;

&lt;p&gt;Simple CRUD works everywhere. Problems start when entities are related.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recipes
 └─ Comments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When relationships become interactive, the usual options are either: splitting everything into multiple pages (losing fluidity), or relying on heavy frameworks that manage state for you (losing transparency and control).&lt;/p&gt;

&lt;p&gt;Tools like Laravel Nova or Filament are powerful and work well in structured teams. But when flows become very specific, it can be hard to understand what is really happening between backend and frontend.&lt;/p&gt;

&lt;p&gt;I wanted a different approach: keep everything in PHP, use fetch to avoid page reloads, but without hiding what’s going on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recipes: CRUD with Relationships, Fully in Fetch
&lt;/h2&gt;

&lt;p&gt;This is the full example: recipes with comments, two interface levels, no page reloads.&lt;/p&gt;

&lt;p&gt;Recipe model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class RecipeModel extends AbstractModel
{
   protected function configure($rule): void
   {
        $rule-&amp;gt;table('#__recipes')
            -&amp;gt;id()
            -&amp;gt;hasMany('comments', RecipeCommentsModel::class, 'recipe_id')
            -&amp;gt;image('image')
            -&amp;gt;title('name')-&amp;gt;index()
            -&amp;gt;text('ingredients')-&amp;gt;formType('textarea')
            -&amp;gt;select('difficulty', ['Easy', 'Medium', 'Hard']);
   }
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In just a few lines, it defines the table, the one-to-many relationship, and the field types. The builders automatically know how to handle uploads, selects, and text areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comments model&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class RecipeCommentsModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule-&amp;gt;table('#__recipe_comments')
            -&amp;gt;id()
            -&amp;gt;int('recipe_id')-&amp;gt;formType('hidden')
            -&amp;gt;text('comment');
    }
} 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else — lists, offcanvas forms, modals, automatic refresh — is built using the same builders and the same mental model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqo6baiayycqiqj0ieg34.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqo6baiayycqiqj0ieg34.gif" alt=" " width="700" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why This Approach&lt;br&gt;
This is not another framework. It’s a way of thinking about admin panels.&lt;br&gt;
Builders reduce boilerplate, but the code remains readable PHP. If a builder doesn’t do what you need, you can always drop down to plain PHP.&lt;br&gt;
JSON as a contract between backend and frontend is easy to debug. No hidden state, no complex lifecycles. PHP sends instructions, the browser executes them.&lt;br&gt;
Most importantly, relationships are handled with the same tools as basic CRUD. There’s no separate system to learn just for relations.&lt;/p&gt;

&lt;p&gt;Who This Is For&lt;br&gt;
If you work with Laravel and you’re happy with it, this is probably not for you.&lt;/p&gt;

&lt;p&gt;But if you find yourself in one of these situations:&lt;/p&gt;

&lt;p&gt;You maintain existing PHP projects that can’t be migrated&lt;br&gt;
You work on internal tools, CRMs, or management systems&lt;br&gt;
You need to understand the code quickly, even months later&lt;br&gt;
You don’t want to chase complex ecosystems&lt;br&gt;
You want PHP that looks and feels like PHP&lt;br&gt;
Then it might be worth rethinking how we build admin interfaces.&lt;/p&gt;

&lt;p&gt;Admin panels are not public websites. They are work tools. And the best tools are those that do their job well, remain understandable over time, and don’t force you to rewrite everything when requirements change.&lt;/p&gt;

&lt;p&gt;These reflections come from the development of MilkAdmin, a PHP admin panel system I actively maintain. Code, demos, and documentation are available at &lt;a href="//milkadmin.org"&gt;milkadmin.org&lt;/a&gt;. MIT licensed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;https://github.com/giuliopanda/milk-admin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
    </item>
    <item>
      <title>Is it time to introduce milkadmin?</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Tue, 25 Nov 2025 15:11:38 +0000</pubDate>
      <link>https://forem.com/giuliopanda/is-it-time-to-introduce-milkadmin-4l2m</link>
      <guid>https://forem.com/giuliopanda/is-it-time-to-introduce-milkadmin-4l2m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2eothumd5zo9xmsosum.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2eothumd5zo9xmsosum.webp" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've been working for some time on a &lt;strong&gt;simple, lightweight core admin for PHP&lt;/strong&gt;. It's a basic administrative environment: login, user &amp;amp; permission management, install and updates. I'm finally ready to talk about it.&lt;/p&gt;

&lt;p&gt;It's an open-source project I called MilkAdmin.&lt;/p&gt;

&lt;p&gt;Installation is "the old way", like WordPress: copy it to the server and you're up and running. You get a minimal environment with access to &lt;strong&gt;two databases&lt;/strong&gt;: one for internal configuration (works well with SQLite) and a second one for your application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;p&gt;I'll keep this short and give a few practical examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  A page in seconds
&lt;/h2&gt;

&lt;p&gt;To create a new page (with a menu entry) you just define a module:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;class CupOfMilk extends AbstractModule&lt;br&gt;
  {&lt;br&gt;
      protected function configure($rule): void&lt;br&gt;
      {&lt;br&gt;
          $rule-&amp;gt;page('cup')&lt;br&gt;
              -&amp;gt;menu('A cup of milk', '', 'bi bi-cup-hot-fill')&lt;br&gt;
              -&amp;gt;access('registered');&lt;br&gt;
      }&lt;br&gt;
  }&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Build Calendar Example&lt;br&gt;
The configure method enforces a clear and consistent structure for module rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attributes to keep code readable
&lt;/h2&gt;

&lt;p&gt;I make heavy use of PHP attributes to identify actions and methods:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#[RequestAction('home')]&lt;br&gt;
  public function helloWorld() {&lt;br&gt;
      Response::render('&amp;lt;h1&amp;gt;Hello World&amp;lt;/h1&amp;gt;', []);&lt;br&gt;
  }&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The models
&lt;/h2&gt;

&lt;p&gt;Models follow a classic approach but use a compact, expressive syntax:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;class MyModel extends AbstractModel&lt;br&gt;
  {&lt;br&gt;
      protected function configure($rule): void&lt;br&gt;
      {&lt;br&gt;
          $rule-&amp;gt;table('#__posts')&lt;br&gt;
              -&amp;gt;id()&lt;br&gt;
              -&amp;gt;title()-&amp;gt;index()&lt;br&gt;
              -&amp;gt;text('content')-&amp;gt;formType('editor')&lt;br&gt;
              -&amp;gt;created_at()-&amp;gt;hideFromEdit()&lt;br&gt;
              -&amp;gt;datetime('updated_at')-&amp;gt;hideFromEdit()-&amp;gt;saveValue(date('Y-m-d H:i:s'));&lt;br&gt;
      }&lt;br&gt;
  }&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-in builders
&lt;/h2&gt;

&lt;p&gt;Even though the core is minimalist, I built a set of builders for tables, forms, lists and other UI elements. In the latest version I added a CalendarBuilder:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviygs3bi0h1ua2rfopbj.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fviygs3bi0h1ua2rfopbj.jpeg" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$calendar = CalendarBuilder::create($this-&amp;gt;model, $calendar_id)&lt;br&gt;
      -&amp;gt;mapFields([&lt;br&gt;
          'id' =&amp;gt; 'id',&lt;br&gt;
          'title' =&amp;gt; 'title',&lt;br&gt;
          'start_datetime' =&amp;gt; 'start_datetime',&lt;br&gt;
          'end_datetime' =&amp;gt; 'end_datetime',&lt;br&gt;
          'class' =&amp;gt; function($event, $event_raw) {&lt;br&gt;
              return $event_raw-&amp;gt;event_class;&lt;br&gt;
          }&lt;br&gt;
      ])&lt;br&gt;
      -&amp;gt;setMonthYear($month, $year)&lt;br&gt;
      -&amp;gt;setHeaderTitle('Events Calendar')&lt;br&gt;
      -&amp;gt;setHeaderIcon('bi-calendar-event')&lt;br&gt;
      -&amp;gt;onAppointmentClick('?page='.$this-&amp;gt;page.'&amp;amp;action=edit&amp;amp;id=%id%')&lt;br&gt;
      -&amp;gt;onEmptyDateClick('?page='.$this-&amp;gt;page.'&amp;amp;action=edit&amp;amp;date=%date%')&lt;br&gt;
      -&amp;gt;setHeaderColor('primary');&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Other features
&lt;/h2&gt;

&lt;p&gt;Very few dependencies.&lt;br&gt;
No external JavaScript libraries. The project includes a small internal system for AJAX responses and theme element handling.&lt;br&gt;
Example: to show a modal from a server response you can return JSON like this:&lt;br&gt;
&lt;code&gt;Response::json([&lt;br&gt;
      'status' =&amp;gt; 'success',&lt;br&gt;
      'modal' =&amp;gt; [&lt;br&gt;
          'action' =&amp;gt; 'show',&lt;br&gt;
          'title' =&amp;gt; 'Hello World',&lt;br&gt;
          'body' =&amp;gt; 'Hello World',&lt;br&gt;
      ]&lt;br&gt;
  ]);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There's also an integrated permission system and attention to security. For now, I do not recommend using MilkAdmin in production projects without a security review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ideal user
&lt;/h2&gt;

&lt;p&gt;I know there are mature, powerful solutions for admin panels. But if you work with WordPress or other PHP platforms, you may not have the time or desire to learn large frameworks, nor to download thousands of files to create a simple table. &lt;/p&gt;

&lt;p&gt;MilkAdmin aims to be a lightweight support system, easy to integrate with other systems, and that lets you fall back to plain PHP when needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Work in progress
&lt;/h2&gt;

&lt;p&gt;MilkAdmin is not the destination, it's a base for building your ideas. My long-term goal is to develop an advanced reporting system; what's yours?&lt;/p&gt;

&lt;p&gt;For a quick experiment, try MilkAdmin on a small personal project: a book list, a link catalog, or a private calendar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;View the repository on GitHub: &lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;https://github.com/giuliopanda/milk-admin&lt;/a&gt; Visit the project site: &lt;a href="https://www.milkadmin.org/" rel="noopener noreferrer"&gt;https://www.milkadmin.org/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip: the Star button is at the top-right corner of the GitHub repository page. If you like the project, please click the star there.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Call to action
&lt;/h2&gt;

&lt;p&gt;If you find MilkAdmin interesting, please give it a try and star the repo. Stars are the first sign of success for an open-source project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Want help formatting this?
&lt;/h2&gt;

&lt;p&gt;I can also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adapt this as a Medium or blog post;&lt;/li&gt;
&lt;li&gt;prepare a LinkedIn announcement;&lt;/li&gt;
&lt;li&gt;turn it into a README or a more technical write-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would you like me to prepare the LinkedIn version as well?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>php</category>
      <category>wordpress</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Reinventing the Wheel: A Reflection on Software Development and Open Source</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Mon, 10 Nov 2025 14:17:09 +0000</pubDate>
      <link>https://forem.com/giuliopanda/reinventing-the-wheel-a-reflection-on-software-development-and-open-source-5494</link>
      <guid>https://forem.com/giuliopanda/reinventing-the-wheel-a-reflection-on-software-development-and-open-source-5494</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;Reinventing the Wheel: A Reflection on Software Development and Open Source&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;I'm working on an open-source project called &lt;a href="https://github.com/giuliopanda/milk-admin" rel="noopener noreferrer"&gt;Milk Admin&lt;/a&gt;: a PHP admin core designed to quickly build supporting administrative backends. The project isn't ready yet, but if you feel like taking a look, I'd be happy. &lt;br&gt;
But that's not what I wanted to talk about.&lt;br&gt;
A couple of months ago, I posted an article about it on an Italian forum. I was very surprised by the comments, and I decided to write this piece to reflect on how the open-source software landscape has changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Time Before Certainties&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When I started developing for the web, the world was split in two: those who used CMSs, and those who wrote everything from scratch. As strange as it may sound today, building from scratch was still a respected approach'also because it showed you knew how to program. The code was often questionable, sure, but there was tolerance: the important thing was that it worked.&lt;/p&gt;

&lt;p&gt;A few years later, using Joomla, I developed an open-source component that found some success. I never knew exactly how many people downloaded or used it, but over time I received thanks, donations, and collaboration proposals.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Rise of Standards (and the Shift in Mindset)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Then something changed in the PHP world. With WordPress in particular, a different model emerged: open source as a showcase for premium versions. A perfectly legitimate business strategy, but one that profoundly reshaped community dynamics. In my small personal experience, I had a terrible time developing WordPress plugins'I found its community truly unpleasant and surprisingly ignorant.&lt;/p&gt;

&lt;p&gt;Even Richard Stallman had raised concerns about the open-source development model. But the world changes, and we have to adapt.&lt;/p&gt;

&lt;p&gt;With the rise of frameworks like Laravel and Symfony came a new mantra: &lt;em&gt;don't reinvent the wheel&lt;/em&gt;. Sensible in theory. But it evolved into something more rigid: the idea that there are 'right' and 'wrong' ways to do things, and that innovation means learning to use existing tools better'not creating new ones.&lt;/p&gt;

&lt;p&gt;Companies embraced this approach enthusiastically: standards mean interchangeability, replaceable resources, fast onboarding. Economically, it makes sense. Culturally, something may have been lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Trial by Fire&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When I introduced the project I mentioned at the start, the reaction I received was almost hostile. It struck me, because in the end it's free code'you can like it or ignore it, but criticizing it feels like an attitude I struggle to understand.&lt;/p&gt;

&lt;p&gt;And the objections weren't even about how it actually worked. Basically, they criticized the fact that more sophisticated tools like Filament already exist. Others complained that it didn't follow modern programming standards. Few seemed to have actually tried the code. The judgment was pre-emptive, based on abstract principles rather than concrete issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why Keep Reinventing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I thought about abandoning the project. But then I stopped to reflect on what drives me to program. It's not just solving problems'that's my job. It's understanding &lt;em&gt;how&lt;/em&gt; things work. It's exploring. It's learning by re-programming the obvious.&lt;/p&gt;

&lt;p&gt;Just last week I was explaining why namespaces are supposed to follow folder structure. Logically, organizing them by concept would make more sense. But it's a standard, so it's not up for discussion. Except'sometimes we &lt;em&gt;should&lt;/em&gt; question whether better ways of doing things might exist. Reflecting on that every now and then isn't a bad thing.&lt;/p&gt;

&lt;p&gt;If everyone stopped building CMSs because 'there's WordPress,' if no one created new frameworks because 'there's Laravel,' how many opportunities would be lost?&lt;br&gt;
If no one had ever reinvented the wheel, our cars today would still be rolling on stone wheels.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
      <category>php</category>
    </item>
    <item>
      <title>What to Choose for Creating a PHP Admin Panel?</title>
      <dc:creator>giuliopanda</dc:creator>
      <pubDate>Mon, 18 Aug 2025 16:34:26 +0000</pubDate>
      <link>https://forem.com/giuliopanda/what-to-choose-for-creating-a-php-admin-panel-2lje</link>
      <guid>https://forem.com/giuliopanda/what-to-choose-for-creating-a-php-admin-panel-2lje</guid>
      <description>&lt;h2&gt;
  
  
  I'm sharing my hands-on tests of installing and making initial modifications to various admin panels.
&lt;/h2&gt;

&lt;p&gt;Companies often need to develop internal administrative systems: dashboards, CRUD panels, reports, notifications, and other management tools. The reasons vary. Even though the initial goal is often to "build something quickly" without reinventing the wheel, the work requires skills, time, and solid architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dilemma: What to Use to Build It?
&lt;/h2&gt;

&lt;p&gt;I tested several open-source products to evaluate the possible options available on the market. In this article, I share my direct experience of installing and modifying some of the most popular open-source products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Laravel-based Solutions
&lt;/h2&gt;

&lt;p&gt;These are the descriptions and considerations found online:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filament&lt;/strong&gt;&lt;br&gt;
Excellent if you're already using Laravel. Filament is a modern admin panel that allows you to create CRUD interfaces, tables, charts, and dashboards with just a few lines of code. It's built around the TALL stack (Tailwind, Alpine, Laravel, Livewire).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Backpack for Laravel&lt;/strong&gt;&lt;br&gt;
A simpler alternative to Filament, based on Bootstrap. Also great for developers with limited experience in Laravel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MoonShine&lt;/strong&gt;&lt;br&gt;
A lightweight and quick-to-configure admin panel. Promises a working CRUD setup in less than 5 minutes. Ideal for getting started quickly.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;My experience:&lt;/strong&gt; I only tested Filament among these. It has extensive documentation. However, navigating Laravel installations for someone who doesn't know it requires some time to get oriented (it took me half an hour to install it, but obviously those who know it do everything in 5 minutes). I encountered some silly problems, but perhaps the documentation, although extensive, wasn't that clear. First you create the Laravel project, then you add Filament - it's obvious, but in haste I had gotten the order wrong. Additionally, the guide assumed you needed to use artisan serve. With regular Apache, the login appeared but wouldn't let you in, and I initially thought the error was related to file permissions. In the end, it took me three-quarters of an hour to access it. To create a "hello world" page, after consulting the documentation I had to ask an AI for help, and thanks to this I succeeded in half an hour, but honestly I don't feel like I understood the structure and how to modify it (and I had even taken a brief Laravel course some time ago). I had to modify 5 different files. The impression is that if you're not a Laravel expert, you risk not being able to separate the code correctly, creating a structure that's difficult to maintain over time. After an hour and a half I have an installation still without user management.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specialized Frameworks
&lt;/h2&gt;

&lt;h4&gt;
  
  
  4. UserFrosting
&lt;/h4&gt;

&lt;p&gt;A framework built specifically for user management, with roles, permissions, modules, and ready-to-use dashboards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My experience:&lt;/strong&gt; The shell installation is well done, but with some small naivety: on the first attempt I set a password that was too short and the system crashed. On the second attempt, everything went well. In 8 minutes I had the server ready and managed to log in. Very nice, although the tables are strangely slow to load. It has everything necessary: users, activities, roles, permissions, and groups. The code is clear, but the documentation a bit less so I had to consult Claude to understand how to create a page. However, after finishing the work I understood quite well how to intervene in the code. Unfortunately, it seems somewhat outdated now. It uses Bootstrap v3.4.1. Total test time: 45 minutes.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. UserSpice
&lt;/h4&gt;

&lt;p&gt;A lightweight solution for basic login, roles, and simple CRUD operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My experience:&lt;/strong&gt; You download the package and install it by opening it from the browser. I admit it's the system I prefer: clean and clear interface, 5 minutes and I'm in. In just 15 minutes I created a new widget by myself. The structure is very well done. After just 20 minutes, in addition to the widget I also created a "hello world" page. Very complete and mature. It doesn't seem to use PSR, but I must say you don't miss it. Well updated, very complete with 57 initial tables. Particular is that on GitHub there's no code present, only documentation and a hotfix folder.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Bonfire (CodeIgniter 4)
&lt;/h4&gt;

&lt;p&gt;A starter kit for CodeIgniter 4, complete with everything necessary to start an administrative app: login, user management, roles, dashboard, logs, settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My experience:&lt;/strong&gt; I had to repeat the installation 3 times: the first time I skipped a command (but the documentation misled me), the second time I had a problem with localhost ports (I think a spark bug). The third time it finally went well and I was able to access the administration. I was happy to have persisted because it's the administrative system I liked most. Essential: User, settings, and tools. What you need to start developing. The user tab is complete: in addition to basic data, we find permissions, access logs, and the ability to change the password. Underneath there's CodeIgniter which is a very solid framework. The settings are particularly well crafted!&lt;/p&gt;

&lt;h4&gt;
  
  
  7. Milk Admin
&lt;/h4&gt;

&lt;p&gt;An admin panel designed for creating your own administrative applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My experience:&lt;/strong&gt; This entire test was done to see the various online products and compare them with the project I'm developing. Milk Admin is a solution aimed more at development like Bonfire or Filament, but which uses an approach close to UserSpice. It installs in 5 minutes from browser and not from shell and doesn't require particular knowledge to start modifying it. It's modular and user and permission management is limited to the essentials to make it as easy possible. It allows you to manage CRUD, APIs, cron jobs, and the product installation update flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;If you're already an expert in a framework, it obviously makes sense to use the tools of that framework, but otherwise the learning curve of Laravel or CodeIgniter can be too steep, especially if you don't plan to have continuous development in that area. In this case, solutions like UserSpice or MilkAdmin can be more immediate and practical.&lt;/p&gt;

</description>
      <category>php</category>
    </item>
  </channel>
</rss>
