<?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>DEV Community: Roberto B.</title>
    <description>The latest articles on DEV Community by Roberto B. (@robertobutti).</description>
    <link>https://dev.to/robertobutti</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%2F243922%2F13c0045e-6656-4f74-aa57-3f9f39716be5.jpeg</url>
      <title>DEV Community: Roberto B.</title>
      <link>https://dev.to/robertobutti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robertobutti"/>
    <language>en</language>
    <item>
      <title>GPX Runner's data decoded with PHP</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Sat, 07 Mar 2026 08:06:36 +0000</pubDate>
      <link>https://dev.to/robertobutti/gpx-runners-data-decoded-with-php-bm9</link>
      <guid>https://dev.to/robertobutti/gpx-runners-data-decoded-with-php-bm9</guid>
      <description>&lt;p&gt;Every runner with a sports watch carries a small data recorder on their wrist. After every run, it produces a GPX file: a detailed log of GPS positions, timestamps, elevation, and often heart rate. Most runners glance at the summary on their phone: distance, time, average pace, and move on.&lt;/p&gt;

&lt;p&gt;But that GPX file holds much more. With the &lt;a href="https://github.com/Hi-Folks/statistics" rel="noopener noreferrer"&gt;hi-folks/statistics&lt;/a&gt; PHP package, you can extract per-kilometer splits and apply real statistical analysis to your running performance, from pacing consistency to elevation impact, cardiac drift to long-term improvement trends.&lt;/p&gt;

&lt;p&gt;In this article, we will parse a GPX file, build per-kilometer splits, and analyze a 10K run step by step using descriptive statistics, correlation, regression, outlier detection, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package requires PHP 8.2+ and is available on GitHub: &lt;a href="https://github.com/Hi-Folks/statistics" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/statistics&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is inside a GPX file?
&lt;/h2&gt;

&lt;p&gt;A GPX (GPS Exchange Format) file is XML. Every sport watch — Garmin, Polar, Suunto, Apple Watch, Coros — can export one. Each file contains a sequence of &lt;strong&gt;trackpoints&lt;/strong&gt; recorded every 1–5 seconds during your run. A trackpoint looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;trkpt&lt;/span&gt; &lt;span class="na"&gt;lat=&lt;/span&gt;&lt;span class="s"&gt;"45.4642"&lt;/span&gt; &lt;span class="na"&gt;lon=&lt;/span&gt;&lt;span class="s"&gt;"9.1900"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ele&amp;gt;&lt;/span&gt;122.4&lt;span class="nt"&gt;&amp;lt;/ele&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;time&amp;gt;&lt;/span&gt;2025-03-15T07:30:45Z&lt;span class="nt"&gt;&amp;lt;/time&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;extensions&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;gpxtpx:TrackPointExtension&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;gpxtpx:hr&amp;gt;&lt;/span&gt;152&lt;span class="nt"&gt;&amp;lt;/gpxtpx:hr&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/gpxtpx:TrackPointExtension&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/extensions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/trkpt&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From these raw points you can derive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distance&lt;/strong&gt; between consecutive points (using the Haversine formula)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pace&lt;/strong&gt; per kilometer (time elapsed over each 1 km segment)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elevation gain and loss&lt;/strong&gt; per kilometer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heart rate&lt;/strong&gt; averages per kilometer (from the Garmin extension)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Parsing the GPX file
&lt;/h2&gt;

&lt;p&gt;PHP's built-in &lt;code&gt;SimpleXML&lt;/code&gt; makes parsing straightforward. Here are the helper functions we use:&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="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;parseGpx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$filePath&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;simplexml_load_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$xml&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cannot parse GPX file: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$filePath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&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;$namespaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$xml&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNamespaces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$xml&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;trk&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;trkseg&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;trkpt&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'lat'&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;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lat'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'lon'&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;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lon'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'ele'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ele&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;strtotime&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'hr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="c1"&gt;// Extract heart rate from Garmin TrackPointExtension&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gpxtpx'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$extensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$trkpt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$extensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$gpxtpx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$extensions&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$namespaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gpxtpx'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gpxtpx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;TrackPointExtension&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;hr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hr'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;$gpxtpx&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;TrackPointExtension&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;hr&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="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$points&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$point&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$points&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To calculate the distance between two GPS coordinates, we use the &lt;strong&gt;Haversine formula&lt;/strong&gt;, the standard method for computing great-circle distance on a sphere:&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="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;haversineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$lat1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$lon1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$lat2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$lon2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6371000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Earth radius in meters&lt;/span&gt;
    &lt;span class="nv"&gt;$dLat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lat2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$lat1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$dLon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lon2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$lon1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dLat&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
       &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lat1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$lat2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dLon&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$R&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we walk through the trackpoints, accumulating distance until we hit each kilometer mark, and record the split:&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="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;buildKmSplits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$splits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nv"&gt;$currentKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$kmDistance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$kmStartTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$kmEleGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$kmEleLoss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$kmHrValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&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;$prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="nv"&gt;$curr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="nv"&gt;$segDist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lat'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lon'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lat'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'lon'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="nv"&gt;$kmDistance&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$segDist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$eleDiff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ele'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ele'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eleDiff&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kmEleGain&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$eleDiff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kmEleLoss&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eleDiff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hr'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kmHrValues&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'hr'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmDistance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$kmTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$kmStartTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$currentKm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$kmTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$kmTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmEleGain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmEleLoss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmHrValues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                    &lt;span class="o"&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="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmHrValues&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="nv"&gt;$currentKm&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$kmDistance&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$kmStartTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$curr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="nv"&gt;$kmEleGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$kmEleLoss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$kmHrValues&lt;/span&gt; &lt;span class="o"&gt;=&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="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;For this article, we use a simulated 10K run with realistic characteristics: a hilly middle section, slight positive split tendency, and heart rate drifting upward with fatigue. If you have a real GPX file, just swap in &lt;code&gt;parseGpx()&lt;/code&gt; and &lt;code&gt;buildKmSplits()&lt;/code&gt;.&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="c1"&gt;// === Option 1: Parse a real GPX file ===&lt;/span&gt;
&lt;span class="c1"&gt;// $trackpoints = parseGpx('your-run.gpx');&lt;/span&gt;
&lt;span class="c1"&gt;// $splits = buildKmSplits($trackpoints);&lt;/span&gt;

&lt;span class="c1"&gt;// === Option 2: Simulated 10K run ===&lt;/span&gt;
&lt;span class="nv"&gt;$splits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;322&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;322&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;145&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;318&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;318&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;335&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;335&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;158&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;348&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;348&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;164&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;162&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;312&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;155&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;325&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;325&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;158&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;338&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;338&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;165&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;352&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;352&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;170&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'time'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'pace'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'avgHr'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;172&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package includes utility classes for column extraction and time formatting:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Stat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Freq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Utils\Arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Utils\Format&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$eleGains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$kmNumbers&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pace'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'avgHr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'km'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The full PHP example with the complete code is here: &lt;a href="https://github.com/Hi-Folks/statistics/blob/main/examples/article-gpx-running-analysis.php" rel="noopener noreferrer"&gt;examples/article-gpx-running-analysis.php&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Run overview, your run at a glance
&lt;/h2&gt;

&lt;p&gt;Before diving into analysis, let's get the big picture:&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;$totalTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$totalEleGain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleGain'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$totalEleLoss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'eleLoss'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Distance:        "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total time:      "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$totalTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Average pace:    "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Elevation gain:  +"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$totalEleGain&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" m"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Elevation loss:  -"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$totalEleLoss&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" m"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Average HR:      "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" bpm"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Distance:        10 km
Total time:      0:55:20
Average pace:    0:05:32/km
Elevation gain:  +117 m
Elevation loss:  -93 m
Average HR:      160 bpm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the summary your watch shows you. Now let's look at what the numbers actually reveal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Pace descriptive statistics, how consistent were you?
&lt;/h2&gt;

&lt;p&gt;The average pace is useful, but it hides the variation. Did you hold a steady 5:32/km throughout, or did you yo-yo between 5:12 and 5:52?&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;$meanPace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$medianPace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stdevPace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$quartiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;quantiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Mean pace:       "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$meanPace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Median pace:     "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$medianPace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Std deviation:   "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$stdevPace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" sec"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Fastest km:      "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Slowest km:      "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Quartiles:       Q1="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"  Q2="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"  Q3="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mean pace:       0:05:32/km
Median pace:     0:05:33/km
Std deviation:   13 sec
Fastest km:      0:05:12/km (km 6)
Slowest km:      0:05:52/km (km 9)
Quartiles:       Q1=0:05:21/km  Q2=0:05:33/km  Q3=0:05:42/km
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;standard deviation of 13 seconds&lt;/strong&gt; means most of your km were within ~13 seconds of the average. That's moderate consistency for a hilly course.&lt;/li&gt;
&lt;li&gt;If &lt;strong&gt;mean and median are close&lt;/strong&gt; (5:32 vs 5:33), your pacing was roughly symmetric — no extreme skew toward fast or slow km.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;range&lt;/strong&gt; (5:12 to 5:52 = 40 seconds) tells you the spread from your best to worst km. Compare this with the IQR (Q1 to Q3 = 21 seconds) — the core of your pacing was much tighter than the extremes suggest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Pacing consistency, did you positive-split or negative-split?
&lt;/h2&gt;

&lt;p&gt;Every coach talks about pacing strategy. A &lt;strong&gt;positive split&lt;/strong&gt; means you slowed down in the second half; a &lt;strong&gt;negative split&lt;/strong&gt; means you got faster. The &lt;strong&gt;Coefficient of Variation (CV)&lt;/strong&gt; puts a single number on your consistency:&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;$cv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;coefficientOfVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$halfPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;intdiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$firstHalfPaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$halfPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$secondHalfPaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$halfPoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$meanFirst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$firstHalfPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$meanSecond&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$secondHalfPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$splitDiff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$meanSecond&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$meanFirst&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$splitPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$splitDiff&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$meanFirst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Coefficient of Variation: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$cv&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"%"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"First half avg pace:  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$meanFirst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Second half avg pace: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$meanSecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Coefficient of Variation: 3.91%
First half avg pace:  0:05:33/km (km 1-5)
Second half avg pace: 0:05:31/km (km 6-10)
Negative split: 1.2 sec/km faster (0.4% improvement)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;CV below 5%&lt;/strong&gt; is considered good pacing for a hilly course. Elite runners often achieve CV under 2% on flat courses.&lt;/li&gt;
&lt;li&gt;Our runner managed a slight &lt;strong&gt;negative split&lt;/strong&gt;, the second half was marginally faster. This is often the sign of disciplined pacing: holding back on the uphills in km 3–5 and then capitalizing on the downhill at km 6.&lt;/li&gt;
&lt;li&gt;Compare your CV across different runs to track whether your pacing discipline is improving over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Elevation impact, how much do hills slow you down?
&lt;/h2&gt;

&lt;p&gt;This is the question every trail runner wants answered. We have per-km elevation gain and per-km pace. Let's see if hills measurably affect your speed:&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;$corrEle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eleGains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$regEle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linearRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eleGains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2Ele&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$eleGains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Correlation (elevation gain vs pace): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$corrEle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Linear regression: pace = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$regEle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" x eleGain + "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$regEle&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"R-squared: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$r2Ele&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Correlation (elevation gain vs pace): 0.8053
Linear regression: pace = 1.18 x eleGain + 318.2
R-squared: 0.6485
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Pearson correlation of 0.81&lt;/strong&gt; is strong, more uphill clearly means slower pace.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;slope (1.18)&lt;/strong&gt; tells you: each additional meter of elevation gain within that kilometer is associated with roughly 1.2 seconds slower pace. On a km with 28m of climbing (km 4), the model predicts you'd run ~33 seconds slower than on a flat km.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R-squared of 0.65&lt;/strong&gt; means elevation gain explains about 65% of the variation in your pace. The remaining 35% comes from other factors — fatigue, wind, terrain surface, mental state.&lt;/li&gt;
&lt;li&gt;Track this slope over multiple runs. As your hill fitness improves, this number should decrease, hills will slow you down less.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Heart rate analysis, detecting cardiac drift
&lt;/h2&gt;

&lt;p&gt;Heart rate tells a story that pace alone cannot. Even if your pace stays constant, a rising heart rate signals that your body is working harder, this is &lt;strong&gt;cardiac drift&lt;/strong&gt;, caused by dehydration, heat, and accumulated fatigue.&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;$meanHr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$stdevHr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Cardiac drift: HR vs km number&lt;/span&gt;
&lt;span class="nv"&gt;$corrHrKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmNumbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$regHrKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linearRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmNumbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2HrKm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$kmNumbers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// HR vs pace&lt;/span&gt;
&lt;span class="nv"&gt;$corrHrPace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mean HR:    160 bpm
Median HR:  160 bpm
Std dev:    8.5 bpm
Min HR:     145 bpm | Max HR: 172 bpm

Cardiac drift (HR vs km):
  Correlation:      0.8506
  Regression:       HR = 2.38 x km + 146.8
  R-squared:        0.7235
  HR drift per km:  +2.4 bpm/km

HR vs pace correlation: 0.6912
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;correlation of 0.85&lt;/strong&gt; between km number and heart rate confirms significant cardiac drift, your heart worked progressively harder as the run continued.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;regression slope (+2.4 bpm/km)&lt;/strong&gt; means your heart rate rose by about 2.4 beats per minute each kilometer. Over 10 km, that's a ~24 bpm increase from start to finish.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;HR vs pace correlation (0.69)&lt;/strong&gt; is moderate — heart rate is influenced by pace, but also by elevation, fatigue, and heat. A perfect correlation would mean pace alone determines HR, which is never true in real-world conditions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Heart rate zone distribution
&lt;/h3&gt;

&lt;p&gt;Using &lt;code&gt;Freq::frequencyTableBySize()&lt;/code&gt;, we can see how many kilometers were spent in each heart rate zone:&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;$hrZones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Freq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;frequencyTableBySize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrValues&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hrZones&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$range&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$range&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" bpm: "&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;str_repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" ("&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" km)"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heart Rate Zone Distribution:
  145 bpm: ## (2 km)
  155 bpm: ##### (5 km)
  165 bpm: ### (3 km)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells you that 5 of your 10 km were in the 150–159 bpm zone — your aerobic sweet spot. Only 3 km pushed into the 165+ range (threshold/anaerobic), primarily on the uphill and late-fatigue segments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Outlier detection, which km was your best and worst?
&lt;/h2&gt;

&lt;p&gt;Not every kilometer is created equal. Some are unusually fast (downhill? adrenaline?) or slow (steep hill? red light? cramp?). The z-score tells you exactly how unusual each km was:&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;$zscores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;zscores&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$split&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  km "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'km'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;": "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$split&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pace'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"  z="&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%+.2f"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$zscores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  km  1: 0:05:22/km  z=-0.77
  km  2: 0:05:18/km  z=-1.08
  km  3: 0:05:35/km  z=+0.23
  km  4: 0:05:48/km  z=+1.23
  km  5: 0:05:40/km  z=+0.62
  km  6: 0:05:12/km  z=-1.54
  km  7: 0:05:25/km  z=-0.54
  km  8: 0:05:38/km  z=+0.46
  km  9: 0:05:52/km  z=+1.54
  km 10: 0:05:30/km  z=-0.15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Negative z-scores&lt;/strong&gt; mean faster than average; &lt;strong&gt;positive&lt;/strong&gt; means slower. The further from zero, the more unusual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Km 6 (z = -1.54)&lt;/strong&gt; was the standout fast km — 20 seconds faster than average. Looking at the data, it had only 2m of elevation gain but 30m of loss. Gravity did the work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Km 9 (z = +1.54)&lt;/strong&gt; was the slowest — 18m of climbing plus accumulated fatigue in the late stages.&lt;/li&gt;
&lt;li&gt;No km exceeded |z| &amp;gt; 2.0, so there are &lt;strong&gt;no statistical outliers&lt;/strong&gt;. This is confirmed by both &lt;code&gt;Stat::outliers()&lt;/code&gt; and &lt;code&gt;Stat::iqrOutliers()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$zOutliers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;outliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// []&lt;/span&gt;
&lt;span class="nv"&gt;$iqrOutliers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;iqrOutliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you had stopped at a traffic light or taken a water break, the affected km would show up as an outlier — and you'd know to exclude it from your pace analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Percentile benchmarks, your pace distribution
&lt;/h2&gt;

&lt;p&gt;Percentiles tell you what your pace range actually looks like across this run:&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;$percentiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$percentiles&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  P"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$p&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;": "&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  P10: 0:05:13/km
  P25: 0:05:21/km
  P50: 0:05:33/km
  P75: 0:05:42/km
  P90: 0:05:52/km
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P10 (5:13/km)&lt;/strong&gt; is the pace you only sustain on your fastest 10% of km — your peak speed on this run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P50 (5:33/km)&lt;/strong&gt; is your median pace — the truest single number for "how fast did I run?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P90 (5:52/km)&lt;/strong&gt; is your slowest 10% — your weakest km, usually hills or the final push.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;gap between P25 and P75&lt;/strong&gt; (21 seconds) is your interquartile range — the "core band" of your pacing. A narrower band means more consistent running.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 8: Distribution shape, are your km skewed?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;skewness()&lt;/code&gt; and &lt;code&gt;kurtosis()&lt;/code&gt; reveal the shape of your pace distribution:&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;$skewness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;skewness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$kurtosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;kurtosis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Skewness: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$skewness&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Kurtosis: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$kurtosis&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Skewness: 0.0481
Kurtosis: -0.9316
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skewness near zero (0.05)&lt;/strong&gt; means your pace distribution is approximately symmetric. You did not have a long tail of slow km or fast km — the variation was balanced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative kurtosis (-0.93)&lt;/strong&gt; means your pace values are more uniformly spread out than a normal distribution, fewer km clustered tightly around the mean, and the extremes are not very extreme. This is typical for a hilly course where terrain forces variation.&lt;/li&gt;
&lt;li&gt;If your skewness were strongly positive (&amp;gt; 0.5), it would mean a tail of slow km, possibly from steep climbs or late-run fatigue. A negative skewness would mean a tail of fast km, perhaps starting too fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 9: Confidence interval, what is your true pace?
&lt;/h2&gt;

&lt;p&gt;Your 10 km give you an average pace, but with more data (more km), that average would stabilize. The confidence interval tells you the range where your "true" comfortable pace likely falls:&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;$ci&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;confidenceInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"95% CI: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ci&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km to "&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ci&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Standard Error of the Mean: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$sem&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" sec"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;95% CI for your true pace: 0:05:24/km to 0:05:40/km
Standard Error of the Mean: 4.1 sec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are &lt;strong&gt;95% confident&lt;/strong&gt; that your true comfortable pace for this effort level and course profile is between 5:24/km and 5:40/km.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;SEM of 4.1 seconds&lt;/strong&gt; is the engine behind this interval. With only 10 km, there's meaningful uncertainty. On a half-marathon (21 km), the SEM would shrink to about 2.8 seconds, and on a marathon (42 km) to about 2 seconds — your confidence interval would become very tight.&lt;/li&gt;
&lt;li&gt;This is useful for race planning: instead of saying "I run 5:32/km pace", you can say "my pace is 5:24–5:40/km on this type of terrain" — a more honest and useful estimate.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 10: Multi-run trend analysis, are you getting faster?
&lt;/h2&gt;

&lt;p&gt;The most powerful analysis comes from loading multiple GPX files across weeks or months. Each run gives you an average pace, and over time, you can see the trend.&lt;/p&gt;

&lt;p&gt;Here we simulate 8 weeks of training data:&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;$weeks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;342&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;337&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;333&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;328&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;326&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;325&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$trendReg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linearRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$trendR2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$trendCorr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;correlation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Trend regression: pace = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trendReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" x week + "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trendReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"R-squared: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$trendR2&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Correlation: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trendCorr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trend regression: pace = -3.39 x week + 349.1
R-squared:        0.9176
Correlation:      -0.9579
Improvement rate:  3.4 seconds/km per week

Predicted pace at week 12: 0:05:08/km
(Extrapolation — use with caution!)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How to interpret the results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;slope (-3.39)&lt;/strong&gt; means you're improving by about 3.4 seconds per km per week on average. That's meaningful and measurable progress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;R-squared of 0.92&lt;/strong&gt; means the linear model explains most of the variance, but not all of it. The remaining 8% hints that the improvement pattern isn't perfectly linear — there's curvature in the data.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;negative correlation (-0.96)&lt;/strong&gt; confirms weeks going up while pace goes down — exactly what improvement looks like.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;prediction&lt;/strong&gt; for week 12 (5:08/km) is an extrapolation. Linear trends don't continue forever — you won't reach 0:00/km eventually. But for short-term planning (next 2–3 weeks), the projection can be a reasonable target.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why linear regression can be misleading for athletic improvement
&lt;/h3&gt;

&lt;p&gt;The linear model predicts a constant improvement of 3.4 seconds per week, forever. Taken to the extreme, it would eventually predict a pace of 0:00/km, which is obviously impossible. The real issue is more subtle: athletic improvement follows a &lt;strong&gt;diminishing returns&lt;/strong&gt; curve. Early gains come fast (beginner effect, neuromuscular adaptation), but as you get fitter, each additional second of improvement requires more training volume and specificity.&lt;/p&gt;

&lt;p&gt;Look at the data closely: the improvements become progressively smaller, from week 1 to week 2 is 8 seconds, but from week 7 to week 8 it's only 1 second. The rate is clearly slowing down, a pattern that linear regression cannot capture because it assumes a constant slope. This is why the linear R² (0.9176) leaves room for improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logarithmic regression, modeling the plateau
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;logarithmicRegression()&lt;/code&gt; method fits the model &lt;strong&gt;y = a × ln(x) + b&lt;/strong&gt;, which naturally produces fast initial improvement that gradually flattens:&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="c1"&gt;// Logarithmic model: pace = a * ln(week) + b&lt;/span&gt;
&lt;span class="nv"&gt;$logReg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;logarithmicRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logWeeks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logR2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logWeeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Logarithmic regression: pace = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" x ln(week) + "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"R-squared: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$logR2&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logarithmic regression: pace = -12.33 x ln(week) + 350.2
R-squared:              0.9987
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logarithmic model has a much higher R² (0.9987 vs 0.9176), indicating that it fits the observed data substantially better than the linear model &lt;strong&gt;for this dataset&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A note about this simple example: with only 8 points, extremely high R² is easy to obtain.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This suggests that the relationship is likely nonlinear: improvement appears rapid initially and then slows over time, a pattern consistent with diminishing returns. While the linear model already provides a strong fit (R² ≈ 0.92), the much higher R² for the logarithmic model indicates that accounting for curvature captures the structure of the data more accurately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing the predictions
&lt;/h3&gt;

&lt;p&gt;The difference becomes clear when you project forward:&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;$linearPrediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$trendReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$trendReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="nv"&gt;$logPrediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$logReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$logReg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Linear prediction week 12:      "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$linearPrediction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Logarithmic prediction week 12:  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Format&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;secondsToTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logPrediction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"/km"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Linear prediction week 12:      0:05:08/km
Logarithmic prediction week 12: 0:05:20/km
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logarithmic model predicts 5:20/km, &lt;strong&gt;12 seconds more conservative&lt;/strong&gt; than the linear model's 5:08/km. At week 20 the gap widens further: the linear model would predict 4:41/km (unlikely for most recreational runners), while the logarithmic model predicts 5:13/km, a more realistic plateau.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which model should you use?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Linear&lt;/th&gt;
&lt;th&gt;Logarithmic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pace = a × week + b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pace = a × ln(week) + b&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assumes&lt;/td&gt;
&lt;td&gt;Constant improvement forever&lt;/td&gt;
&lt;td&gt;Fast early gains, gradual plateau&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-term (4 weeks)&lt;/td&gt;
&lt;td&gt;Good approximation&lt;/td&gt;
&lt;td&gt;Good approximation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-term (12+ weeks)&lt;/td&gt;
&lt;td&gt;Over-optimistic&lt;/td&gt;
&lt;td&gt;More realistic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Short training blocks, beginners with stable gains&lt;/td&gt;
&lt;td&gt;Multi-month planning, experienced runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R² on this data&lt;/td&gt;
&lt;td&gt;0.9176&lt;/td&gt;
&lt;td&gt;0.9987&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation&lt;/strong&gt;: compare R² values for both models on your own data. If the logarithmic R² is higher, your improvement is already following a curve and the logarithmic model will give more trustworthy projections. If R² values are similar, you're still in the early "linear" phase of improvement — but use the logarithmic model for any prediction beyond 4–6 weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  All four models compared — letting the numbers decide
&lt;/h3&gt;

&lt;p&gt;Rather than assuming which model fits best, let's run all four regression types on the same data and compare them objectively. The package provides &lt;code&gt;logarithmicRegression()&lt;/code&gt;, &lt;code&gt;powerRegression()&lt;/code&gt;, and &lt;code&gt;exponentialRegression()&lt;/code&gt; alongside &lt;code&gt;linearRegression()&lt;/code&gt; — each fits a different curve shape:&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="c1"&gt;// Linear: pace = a * week + b&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$aLin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bLin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linearRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2Lin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Logarithmic: pace = a * ln(week) + b&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$aLog&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bLog&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;logarithmicRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logWeeks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2Log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logWeeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Power: pace = a * week^b&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$aPow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bPow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;powerRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logPaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2Pow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logWeeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$logPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Exponential: pace = a * e^(b * week)&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$aExp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bExp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exponentialRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$r2Exp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rSquared&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$logPaces&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model             R²       Week 12    Week 20    Week 52
─────────────────────────────────────────────────────────
Linear            0.9176   0:05:08    0:04:41    0:02:53
Logarithmic       0.9987   0:05:20    0:05:13    0:05:02
Power             0.9985   0:05:20    0:05:14    0:05:03
Exponential       0.9232   0:05:09    0:04:45    0:03:27
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The R² column settles it without any assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logarithmic (R² = 0.9987)&lt;/strong&gt; and &lt;strong&gt;Power (R² = 0.9985)&lt;/strong&gt; are virtually tied and both fit the data near-perfectly. They capture the curvature that the other two models miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear (R² = 0.9176)&lt;/strong&gt; and &lt;strong&gt;Exponential (R² = 0.9232)&lt;/strong&gt; leave about 8% of the variance unexplained — they force a shape that doesn't match the data's natural curve.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But R² only measures how well a model fits &lt;em&gt;past&lt;/em&gt; data. The &lt;strong&gt;prediction columns&lt;/strong&gt; reveal which models are trustworthy for the future:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At &lt;strong&gt;week 20&lt;/strong&gt;, linear predicts 4:41/km and exponential predicts 4:45/km — both assume improvement keeps accelerating at nearly the same rate. For a recreational runner who started at 5:50/km, breaking 5:00/km in just 20 weeks is ambitious; breaking 4:45 is unrealistic.&lt;/li&gt;
&lt;li&gt;At &lt;strong&gt;week 52&lt;/strong&gt; (one year), linear predicts 2:53/km — faster than the world record marathon pace (2:50/km for Kelvin Kiptum's 2:00:35). Exponential predicts 3:27/km. Both are absurd for the same runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logarithmic&lt;/strong&gt; and &lt;strong&gt;Power&lt;/strong&gt; predict 5:02/km and 5:03/km at week 52 — a realistic plateau where the runner has improved by about 48 seconds over a year and further gains require significantly more effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The logarithmic and power models converge on nearly identical predictions because they both model the same fundamental pattern: fast early gains that asymptotically flatten. For running pace data, either is a sound choice. Logarithmic is slightly simpler to interpret (the coefficient &lt;code&gt;a&lt;/code&gt; directly tells you "seconds of improvement per unit of ln(week)"), which is why we recommend it as the default for trend analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visualizing the models: why the curve matters more than the fit
&lt;/h3&gt;

&lt;p&gt;These charts plot each model's forecast beyond the training data, so you can see at a glance where the predictions stay realistic and where they drift into fantasy.&lt;/p&gt;

&lt;p&gt;The chart below shows all four models fitted to the same training data. The actual pace values (blue dots) end at week 8 — everything beyond is a prediction. Notice how the linear and exponential lines keep diving, while the logarithmic and power curves flatten into a realistic plateau.&lt;/p&gt;

&lt;h4&gt;
  
  
  Linear
&lt;/h4&gt;

&lt;p&gt;The straight line fits the training period reasonably well, but projects an impossible pace of 2:53/km at week 52 — a reminder that constant improvement is a mathematical fiction.&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%2Fxe0cq0b37in94pvcrult.png" 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%2Fxe0cq0b37in94pvcrult.png" alt="The Linear Regression" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Logarithmic
&lt;/h4&gt;

&lt;p&gt;The curve mirrors how runners actually improve: rapid early gains that gradually flatten, predicting a realistic 5:02/km plateau after one year of training.&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%2F3gij9nbf4ynuhqwhqzxe.png" 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%2F3gij9nbf4ynuhqwhqzxe.png" alt="The Logarithmic Regression" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Power
&lt;/h4&gt;

&lt;p&gt;Nearly indistinguishable from the logarithmic model, the power curve confirms the diminishing-returns pattern, two different equations arriving at the same truth.&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%2F1txkrcdvmdn3yszyxcr2.png" 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%2F1txkrcdvmdn3yszyxcr2.png" alt="Power Regression" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Exponential
&lt;/h4&gt;

&lt;p&gt;Slightly better than linear but still too optimistic, the exponential model bends just enough to look plausible in the short term while still predicting an unrealistic 3:27/km at week 52.&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%2Fx5i2ot679g9apor285fn.png" 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%2Fx5i2ot679g9apor285fn.png" alt="Exponential Regression" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Linear VS Logarithmic
&lt;/h4&gt;

&lt;p&gt;When we isolate the two best-fitting models, the difference becomes subtle over the training period but significant in the projection zone. Both track the actual data closely through week 8, but the linear model keeps promising improvement that will never come.&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%2Fqsrd42rmea3h2yryzdza.png" 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%2Fqsrd42rmea3h2yryzdza.png" alt="Linear VS Logarithmic" width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading real GPX files
&lt;/h3&gt;

&lt;p&gt;To implement this with real GPX files, load each file, compute the average pace, and build your arrays:&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;$gpxFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'runs/2025-*.gpx'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$weeks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gpxFiles&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$trackpoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseGpx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$splits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildKmSplits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$trackpoints&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$splits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'pace'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$paces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Compare both models&lt;/span&gt;
&lt;span class="nv"&gt;$linear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;linearRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$logarithmic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;logarithmicRegression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$weeks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weeklyPaces&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to look for when you analyze your own runs
&lt;/h2&gt;

&lt;p&gt;Run the example and then try it with your own GPX files. Here's what to watch for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CV below 5%&lt;/strong&gt;: your pacing is disciplined. Above 8%: investigate what's causing the variation (hills? starting too fast?).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elevation slope&lt;/strong&gt;: track this number over months. As your hill strength improves, each meter of climb should cost fewer seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cardiac drift slope&lt;/strong&gt;: a lower bpm/km slope means better aerobic fitness and hydration. Compare this across similar runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Z-scores&lt;/strong&gt;: any km with |z| &amp;gt; 2 deserves investigation — was it a genuine outlier (stoppage, cramp) or a terrain feature?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week-over-week trend&lt;/strong&gt;: a negative slope means improvement. Plateaus are normal; a positive slope (getting slower) may signal overtraining or insufficient recovery.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The complete example
&lt;/h2&gt;

&lt;p&gt;The full script with all helper functions and simulated data is available in the repository. You can run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php examples/article-gpx-running-analysis.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Source: &lt;a href="https://github.com/Hi-Folks/statistics/blob/main/examples/article-gpx-running-analysis.php" rel="noopener noreferrer"&gt;examples/article-gpx-running-analysis.php&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary of the package features used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mean()&lt;/code&gt;, &lt;code&gt;median()&lt;/code&gt;, &lt;code&gt;stdev()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Basic descriptive statistics on pace, HR, elevation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;quantiles()&lt;/code&gt;, &lt;code&gt;percentile()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Pace distribution — where do your km fall?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coefficientOfVariation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single number for pacing consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;correlation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Elevation vs pace, HR vs pace, HR vs time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;linearRegression()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quantify hill cost, cardiac drift rate, improvement trend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;logarithmicRegression()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model diminishing returns (pace improvement plateau)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;powerRegression()&lt;/code&gt;, &lt;code&gt;exponentialRegression()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Alternative non-linear trend models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rSquared()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How well does elevation/time explain your pace?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zscores()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flag unusual km segments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;outliers()&lt;/code&gt;, &lt;code&gt;iqrOutliers()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Detect anomalous km (stops, sprints)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;skewness()&lt;/code&gt;, &lt;code&gt;kurtosis()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Distribution shape of your pace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;confidenceInterval()&lt;/code&gt;, &lt;code&gt;sem()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Estimate your true pace range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Freq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frequencyTableBySize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Heart rate zone distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Arr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;extract()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extract columns from split data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;secondsToTime()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human-readable pace and time formatting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Install it and start exploring your own runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>gpx</category>
      <category>php</category>
      <category>statistics</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Exploring Olympic Downhill Results with PHP Statistics</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Fri, 20 Feb 2026 20:57:54 +0000</pubDate>
      <link>https://dev.to/robertobutti/exploring-olympic-downhill-results-with-php-statistics-3eo1</link>
      <guid>https://dev.to/robertobutti/exploring-olympic-downhill-results-with-php-statistics-3eo1</guid>
      <description>&lt;p&gt;When Franjo von Allmen crossed the finish line in 111.61 seconds at the 2026 Olympic Downhill, he claimed gold. But the raw leaderboard only tells part of the story. How dominant was his performance? How tightly packed was the field? Were there outliers that skewed the results?&lt;/p&gt;

&lt;p&gt;With the &lt;a href="https://github.com/Hi-Folks/statistics" rel="noopener noreferrer"&gt;hi-folks/statistics&lt;/a&gt; PHP package, you can answer these questions using the same statistical tools that data scientists rely on, right from your PHP code.&lt;/p&gt;

&lt;p&gt;In this article, we will walk through a real step-by-step analysis of the 2026 Olympic Men's Downhill race results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package requires PHP 8.2+, and you can find the sources here on GitHub: &lt;a href="https://github.com/Hi-Folks/statistics" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/statistics&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;We start with the official race results, 34 athletes, each with a name and finish time in seconds:&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;$results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Franjo von ALLMEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;111.61&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Giovanni FRANZONI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;111.81&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Dominik PARIS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;112.11&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 28 more athletes ...&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Dmytro SHEPIUK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;120.11&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Cormac COMERFORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;124.4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I extracted the times into a flat array so we can feed them into the statistical functions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The full PHP example and the full PHP data array are here: &lt;a href="https://github.com/Hi-Folks/statistics/blob/main/examples/article-downhill-ski-analysis.php" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/statistics/blob/main/examples/article-downhill-ski-analysis.php&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Descriptive statistics, understanding the shape of the data
&lt;/h2&gt;

&lt;p&gt;Before building any model, we need to understand the raw numbers. The &lt;code&gt;Stat&lt;/code&gt; class gives us the essential summary in a few lines:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Stat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$median&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$quartiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;quantiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Sample size: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Mean time: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" seconds"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Median time: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$median&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" seconds"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Standard deviation: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$std&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" seconds"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Min: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$min&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s | Max: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$max&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s | Range: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Quartiles (Q1, Q2, Q3): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s, "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s, "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quartiles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
Sample size: 34
Mean time:   114.38 seconds
Median time: 113.60 seconds
Std dev:     2.60 seconds
Min: 111.61s | Max: 124.4s | Range: 12.79s
Quartiles (Q1, Q2, Q3): 112.95s, 113.60s, 114.77s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things stand out immediately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The mean (114.38) is higher than the median (113.60)&lt;/strong&gt;. In a perfectly symmetric distribution, these two values are identical. When the mean is noticeably higher, it signals that the data is right-skewed; a few slow times are pulling the average up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The range is 12.79 seconds&lt;/strong&gt;, which is enormous for a downhill race. Most athletes finished within a 3-second window (Q1 to Q3: 112.95 to 114.77), but the tail extends to 124.4 seconds (this kind of data, like timing in a downhill competition, depends on the events that occur during the race).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The standard deviation is 2.60 seconds&lt;/strong&gt;. This single number summarizes how spread out the results are. We will use it extensively in the next steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;Stat::quantiles()&lt;/code&gt; method divides the data into four equal groups. The result tells us that 25% of athletes finished under 112.95s (Q1), 50% under 113.60s (Q2/median), and 75% under 114.77s (Q3).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1b: Robust central tendency
&lt;/h3&gt;

&lt;p&gt;The mean (114.38s) is pulled upward by a few slow finishers — Comerford at 124.4s alone adds almost a full second of bias. The &lt;strong&gt;trimmed mean&lt;/strong&gt; solves this by removing a fraction of the extreme values from each end before computing the average:&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;$trimmedMean10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;trimmedMean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$trimmedMean20&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;trimmedMean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Regular mean:       "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Trimmed mean (10%): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$trimmedMean10&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Trimmed mean (20%): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$trimmedMean20&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
Regular mean:       114.38s
Trimmed mean (10%): 113.91s
Trimmed mean (20%): 113.76s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;trimmedMean()&lt;/code&gt; method takes a &lt;code&gt;$proportionToCut&lt;/code&gt; parameter, the fraction to remove from &lt;strong&gt;each&lt;/strong&gt; side. With &lt;code&gt;0.1&lt;/code&gt;, we remove the 3 fastest and 3 slowest athletes (10% of 34 from each end), and the result drops by almost half a second. With &lt;code&gt;0.2&lt;/code&gt;, it drops further to 113.76s — much closer to the median (113.60s).&lt;/p&gt;

&lt;p&gt;This tells us something important: the "typical" downhill time is closer to 113.8s than to 114.4s. The regular mean overstates it because of the tail. When you want a single number to describe "how fast did most athletes go?", the trimmed mean is a better answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1c: Percentile analysis
&lt;/h3&gt;

&lt;p&gt;Quartiles divide the data into 4 groups. But sometimes you want more precision "what time do you need to be in the top 10%?" The &lt;code&gt;percentile()&lt;/code&gt; method lets you query any point on the distribution:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"P10: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s — elite threshold"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"P25: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s — top quarter"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"P50: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s — median"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"P75: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s — bottom quarter"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"P90: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s — struggling"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
P10: 112.21s — elite threshold
P25: 112.95s — top quarter
P50: 113.6s — median
P75: 114.77s — bottom quarter
P90: 118.2s — struggling
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the gap between P75 (114.77s) and P90 (118.2s), over 3 seconds. Compare that to the gap between P10 (112.21s) and P25 (112.95s), less than a second. This asymmetry is the right skew we saw earlier, now quantified: the bottom of the field is much more spread out than the top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1d: Precision of the mean
&lt;/h3&gt;

&lt;p&gt;How precise is our mean of 114.38s? With only 34 athletes, there is some uncertainty. The "Standard Error of the Mean (SEM)" quantifies this:&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;$sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SEM: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$sem&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"95% confidence interval: "&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.96&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$sem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s to "&lt;/span&gt;
    &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.96&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$sem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
SEM: 0.45s
95% confidence interval: 113.51s to 115.25s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SEM is the standard deviation divided by the square root of the sample size: &lt;code&gt;stdev / sqrt(n)&lt;/code&gt;. With 34 athletes, we can estimate that the "true" mean time for this course and field falls within roughly ±0.87s of our calculated mean, at 95% confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Fitting a normal distribution
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;normal distribution&lt;/strong&gt; (the classic bell curve) is one of the most widely used models in statistics. It is defined by just two parameters: the mean (mu) and the standard deviation (sigma). The &lt;code&gt;NormalDist&lt;/code&gt; class lets you build one directly from your data:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\NormalDist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$normal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NormalDist&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSamples&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"mu (mean): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMeanRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// 114.38&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sigma (std dev): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getSigmaRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2.60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;fromSamples()&lt;/code&gt; method calculates both parameters from the data and returns a &lt;code&gt;NormalDist&lt;/code&gt; object. From this point forward, you can ask probabilistic questions about the distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking the fit: model median vs actual median
&lt;/h3&gt;

&lt;p&gt;A quick sanity check: in a perfect normal distribution, the mean, median, and mode are all equal. We can verify this with &lt;code&gt;getMedian()&lt;/code&gt;:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Model median: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMedianRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 114.38&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Actual median: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$median&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// 113.60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model median is 114.38 (equal to the mean, as expected), but the actual median is 113.60, a gap of &lt;strong&gt;0.78 seconds&lt;/strong&gt;. This confirms what we suspected: the data is right-skewed, and the normal distribution is not a perfect fit.&lt;/p&gt;

&lt;p&gt;This is an important lesson: &lt;strong&gt;a normal distribution assumes symmetry&lt;/strong&gt;. When your data has a long tail in one direction (as race results often do), the model will be an approximation. It is still useful for insight, but you should be aware of its limitations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Asking probabilistic questions
&lt;/h2&gt;

&lt;p&gt;Once you have a &lt;code&gt;NormalDist&lt;/code&gt; object, you can ask questions that would be difficult to answer from raw data alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  "What percentage of racers finished under 113 seconds?"
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Cumulative Distribution Function (CDF)&lt;/strong&gt; answers exactly this. It returns the probability that a random value from the distribution falls at or below a given point:&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;$target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;113.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$probUnder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cdfRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Model: P(time &amp;lt;= 113s) = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$probUnder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"%"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Model: 29.8%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model predicts 29.8%. The actual count is 9 out of 34 athletes (26.5%). The gap is a direct consequence of the skewness we identified earlier: the inflated mean shifts the entire curve to the right, making the model slightly overestimate the proportion of fast finishers.&lt;/p&gt;

&lt;h3&gt;
  
  
  "How likely is a time of exactly 113 seconds?"
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Probability Density Function (PDF)&lt;/strong&gt; gives the relative likelihood of a specific value:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PDF at 113s = "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pdfRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;113.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 0.133488&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PDF value is not a probability (it can exceed 1.0 for narrow distributions), but it lets you compare how likely different times are relative to each other. A higher PDF value means that time is more "typical" for this distribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Performance thresholds with the inverse CDF
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Inverse CDF&lt;/strong&gt; works backwards: given a probability, it returns the corresponding value. This is perfect for setting performance thresholds:&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;$eliteThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invCdfRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$slowThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;invCdfRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Top 20% fastest (below): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$eliteThreshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 112.19&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Slowest 20% (above): "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$slowThreshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// 116.56&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the model, a time under 112.19 seconds would place an athlete in the top 20%, while a time above 116.56 seconds would place an athlete in the slowest 20%. In practice, only 3 out of 34 athletes (about 9%) broke the 112.19s barrier, another sign that the skewness pushes the model's "elite" threshold to an unrealistically fast time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Z-scores, measuring performance in standard deviations
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;z-score&lt;/strong&gt; expresses a value as the number of standard deviations away from the mean. Negative z-scores mean faster than average, positive means slower:&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;$z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;zscoreRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;111.61&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// -1.06&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Von Allmen's gold-medal time has a z-score of -1.06; he finished about one full standard deviation faster than the average racer. Here is how the z-scores look across the field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Franjo von ALLMEN     111.61s   z: -1.06   Elite
Giovanni FRANZONI     111.81s   z: -0.99   Elite
Dominik PARIS         112.11s   z: -0.87   Elite
Marco ODERMATT        112.31s   z: -0.80   Strong
...
Jan ZABYSTRAN         114.39s   z: +0.01   Average
...
Cormac COMERFORD      124.40s   z: +3.86   Below avg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Z-scores are powerful because they are &lt;strong&gt;universal and comparable&lt;/strong&gt;. A z-score of -1.06 means the same thing whether you are analyzing ski times, exam scores, or manufacturing tolerances. Comerford's z-score of +3.86 immediately flags him as an extreme outlier, nearly 4 standard deviations from the mean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5b: Outlier detection
&lt;/h3&gt;

&lt;p&gt;Looking at the z-scores, Comerford's +3.86 stands out dramatically. But how do we systematically identify outliers instead of eyeballing them? The package provides two methods, each with different strengths.&lt;/p&gt;

&lt;h4&gt;
  
  
  Method 1: Z-score based detection
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;outliers()&lt;/code&gt; method flags values whose absolute z-score exceeds a threshold. The default is 3.0 (the classic "three sigma rule"), but for a small dataset like ours, 2.5 is more practical:&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;$zscoreOutliers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;outliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$zscoreOutliers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cd"&gt;/**
  124.4s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only Comerford is flagged. The z-score method relies on the mean and standard deviation, and here's the catch, "the outliers themselves inflate the stdev", making it harder to detect them. It's a bit like asking a group that includes a giant "is anyone unusually tall?", the giant's height raises the average, making everyone seem more normal.&lt;/p&gt;

&lt;h4&gt;
  
  
  Method 2: IQR-based detection (box plot whiskers)
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;iqrOutliers()&lt;/code&gt; method uses the Interquartile Range, which is based on quartiles, not the mean. This makes it robust: outliers don't influence the detection mechanism.&lt;/p&gt;

&lt;p&gt;A value is flagged if it falls below &lt;code&gt;Q1 - 1.5 * IQR&lt;/code&gt; or above &lt;code&gt;Q3 + 1.5 * IQR&lt;/code&gt;:&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;$iqrOutliers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;iqrOutliers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$iqrOutliers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$time&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cd"&gt;/**
  119.24s
  120.11s
  124.4s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IQR method catches three outliers: Opmanis (119.24s), Shepiuk (120.11s), and Comerford (124.4s). These are the athletes whose times are far enough from the main pack to fall outside the "whiskers" of a box plot.&lt;/p&gt;

&lt;p&gt;This is the key difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Z-score detection&lt;/em&gt;: assumes a roughly normal distribution, sensitive to the outliers themselves&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;IQR detection&lt;/em&gt;: makes no distributional assumptions, robust to extreme values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For skewed data like race results, IQR is generally the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Classifying athletes into tiers
&lt;/h2&gt;

&lt;p&gt;Combining the CDF with custom thresholds, we can classify every athlete into a performance tier:&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;$tierDefinitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Elite"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Strong"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Average"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"max"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Below avg"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$percentile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Below avg"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tierDefinitions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$def&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$percentile&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nv"&gt;$def&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'max'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$tier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$def&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'label'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="k"&gt;break&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;$z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normal&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;zscoreRounded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$r&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="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'time'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$tier&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"  z: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$z&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;CDF&lt;/strong&gt; returns the percentile rank of each athlete within the model. An athlete at the 15th percentile (like Franzoni) has, according to the model, 85% of the distribution above them. Combined with the z-score, you get a complete picture: tier for quick classification, z-score for precise measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Visualizing the distribution with a frequency table
&lt;/h2&gt;

&lt;p&gt;A frequency table groups data into classes and counts how many values fall into each class. The &lt;code&gt;Freq&lt;/code&gt; class makes this straightforward:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Freq&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$freqTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Freq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;frequencyTableBySize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$freqTable&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;str_repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;" ("&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$count&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;")"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;111s  ** (2)
112s  ****** (6)
113s  ************ (12)
114s  ******** (8)
115s  * (1)
116s   (0)
117s  ** (2)
118s   (0)
119s  * (1)
120s  * (1)
124s  * (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The histogram reveals the story: a dense core of athletes between 111-115 seconds, then a gap, then a handful of stragglers trailing off to 124 seconds. This is the right skew in plain sight. A perfect bell curve would be symmetric around the mean; this clearly is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Measuring the shape of the distribution with skewness and kurtosis
&lt;/h2&gt;

&lt;p&gt;In Step 7 the frequency table hinted at something: the results are not symmetric.&lt;br&gt;
There is a dense core of fast times on the left, and a long tail stretching right toward 124 seconds. But how asymmetric is the distribution, exactly? And how extreme are those outliers?&lt;/p&gt;

&lt;p&gt;Two statistics answer these questions: skewness and kurtosis.&lt;/p&gt;

&lt;p&gt;Skewness measures asymmetry. A value of 0 means perfectly balanced. Positive means the tail extends to the right; negative means it extends to the left.&lt;/p&gt;

&lt;p&gt;Kurtosis (excess) measures tailedness. How much data lives in the extreme tails compared to a normal distribution. A value of 0 means normal-like tails. Positive means heavier tails with more extreme outliers.&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\Statistics\Stat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$skewness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;skewness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 2.3114&lt;/span&gt;

&lt;span class="nv"&gt;$kurtosis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;kurtosis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 6.3361&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A skewness of 2.31 confirms what the frequency table showed visually: most racers finished with similar times clustered together, but a handful of really slow finishers (119s, 120s, 124s) drag the average up. The results are not balanced around the middle, there is a long tail of slow times on the right side.&lt;/p&gt;

&lt;p&gt;A kurtosis of 6.34 tells us something extra that the frequency table alone could not: those slow finishers are not just a little slow, they are very far from the pack. The gap between the main group and the slowest racers is much larger than you would expect in a typical bell curve. The results have extreme outliers.&lt;/p&gt;

&lt;p&gt;Together, these two numbers paint a clear picture: the race had a tight competitive group up front, and a few athletes who were significantly off the pace, making the distribution lopsided (skewness) with extreme stragglers (kurtosis).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;skewness()&lt;/code&gt; method uses the adjusted Fisher-Pearson formula, the same used by Excel's &lt;code&gt;SKEW()&lt;/code&gt; and Python's &lt;code&gt;scipy.stats.skew(bias=False)&lt;/code&gt;.&lt;br&gt;
The &lt;code&gt;kurtosis()&lt;/code&gt; method returns the excess kurtosis, matching Excel's &lt;code&gt;KURT()&lt;/code&gt; and Python's &lt;code&gt;scipy.stats.kurtosis(bias=False)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 9: Dispersion beyond standard deviation
&lt;/h2&gt;

&lt;p&gt;The standard deviation (2.60s) tells us how spread out the times are, but it's heavily influenced by the slow outliers. Are the times really that spread out, or is it just a few athletes pulling the number up?&lt;/p&gt;

&lt;p&gt;Two alternative measures give us a clearer picture:&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;$stdev&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;stdev&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$mad&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;meanAbsoluteDeviation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$medianAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;medianAbsoluteDeviation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Standard deviation:       "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$stdev&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Mean Absolute Deviation:  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$mad&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Median Absolute Deviation: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$medianAD&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
Standard deviation:       2.5974s
Mean Absolute Deviation:  1.7056s
Median Absolute Deviation: 0.88s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The &lt;em&gt;Mean Absolute Deviation&lt;/em&gt; (1.71s) uses absolute differences instead of squared ones, so it's less sensitive to extreme values. It tells us "on average, athletes finish about 1.7 seconds from the mean."&lt;/li&gt;
&lt;li&gt;The &lt;em&gt;Median Absolute Deviation&lt;/em&gt; (0.88s) goes further, it measures the median distance from the median, making it highly resistant to outliers. It tells us the "core pack" of athletes is within about 1 second of each other.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between stdev (2.60s) and median absolute deviation (0.88s) is striking. It reveals that the field has two distinct groups: a tightly packed main group separated by less than a second, and a handful of stragglers who inflate the traditional dispersion measures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Coefficient of Variation — comparing race tightness
&lt;/h2&gt;

&lt;p&gt;How tight was this race? Standard deviation alone doesn't answer this, because it depends on the scale. A stdev of 2.6s means something very different in a 2-minute downhill than in a 30-second sprint.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;Coefficient of Variation&lt;/em&gt; (CV) expresses dispersion as a percentage of the mean, making it comparable across different events:&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;$cvFull&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;coefficientOfVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$top10&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$cvTop10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;coefficientOfVariation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$top10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Full field CV: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$cvFull&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"%"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Top 10 CV:     "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$cvTop10&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"%"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
Full field CV: 2.27%
Top 10 CV:     0.45%
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A CV of 2.27% means the times vary by about 2.3% around the mean, a relatively tight field. But the top 10 is &lt;em&gt;five times tighter&lt;/em&gt; at just 0.45%. This quantifies what every ski fan knows intuitively: at the top, hundredths of a second matter. The margin between gold and 10th place (1.59s) is tiny compared to the margin between 10th and last (11.2s).&lt;/p&gt;

&lt;p&gt;You could use CV to compare across sports or across different race editions: "Was the 2026 downhill tighter than 2022?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Weighted median — focus on the contenders
&lt;/h2&gt;

&lt;p&gt;In a race analysis, not all athletes carry equal weight. The top-seeded racers are the main contenders, their times define the competitive field. The &lt;code&gt;weightedMedian()&lt;/code&gt; method lets us give more importance to specific athletes:&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;$weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$weights&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// top-15 seeded athletes weighted 3x&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$wMedian&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;weightedMedian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Regular median:  "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stat&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$times&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Weighted median: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$wMedian&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"s"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cd"&gt;/**
Regular median:  113.6s
Weighted median: 113.28s
*/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weighted median (113.28s) is lower than the regular median (113.60s) because we're giving triple weight to the top 15 athletes, whose times cluster in the faster range. This answers a different question: "What does a &lt;em&gt;competitive&lt;/em&gt; time look like?", as opposed to "What does the &lt;em&gt;typical&lt;/em&gt; time look like?"&lt;/p&gt;

&lt;p&gt;This is useful for race analysis, but also in any domain where observations have different importance: survey responses with population weights, financial data with volume weights, or sensor readings with reliability weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the normal distribution works (and when it doesn't)
&lt;/h2&gt;

&lt;p&gt;The normal distribution is a great starting point for exploratory analysis, but it assumes &lt;strong&gt;symmetry&lt;/strong&gt;. In our race data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;mean (114.38s) diverges from the median (113.60s)&lt;/strong&gt; by 0.78 seconds&lt;/li&gt;
&lt;li&gt;The model predicts &lt;strong&gt;29.8%&lt;/strong&gt; of racers under 113s, while the actual figure is &lt;strong&gt;26.5%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Extreme outliers (Comerford at z = +3.86) have disproportionate influence on the model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This happens because Olympic downhill races have a natural lower bound (you can only ski so fast) but no upper bound (injuries, mistakes, and less experienced athletes create a long right tail).&lt;/p&gt;

&lt;p&gt;For data like this, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Using the median and quartiles&lt;/strong&gt; (from &lt;code&gt;Stat::median()&lt;/code&gt; and &lt;code&gt;Stat::quantiles()&lt;/code&gt;) for robust summary statistics that are not affected by outliers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trimming outliers&lt;/strong&gt; before fitting the distribution, removing the slowest 3-4 finishers, would dramatically improve the normal fit for the competitive core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploring other distributions&lt;/strong&gt; in future analysis (log-normal distributions often fit race times better, as they naturally handle the right-skew)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, even an imperfect normal model gave us actionable insights: z-scores, percentile-based tiers, and probability estimates that go far beyond a simple leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete example
&lt;/h2&gt;

&lt;p&gt;The full script is available in the repository at &lt;a href="https://github.com/Hi-Folks/statistics/blob/main/examples/norm_dist.php" rel="noopener noreferrer"&gt;&lt;code&gt;examples/norm_dist.php&lt;/code&gt;&lt;/a&gt;. You can run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php examples/norm_dist.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary of the package features used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mean()&lt;/code&gt;, &lt;code&gt;median()&lt;/code&gt;, &lt;code&gt;stdev()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Basic descriptive statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;quantiles()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Divide data into equal-probability intervals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fromSamples()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Build a normal distribution from raw data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getMedian()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model median (equals mean for normal dist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cdf()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Probability of a value or lower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pdf()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Relative likelihood of a specific value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;invCdf()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Find the value for a given probability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NormalDist&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zscore()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard deviations from the mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Freq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frequencyTableBySize()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Group data into classes for histograms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trimmedMean()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mean after removing outliers from each side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;percentile()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Value at any percentile (0–100)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weightedMedian()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Median with weighted observations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sem()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard error of the mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;coefficientOfVariation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Relative dispersion as a percentage (CV%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meanAbsoluteDeviation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Average distance from the mean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;medianAbsoluteDeviation()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Median distance from the median (robust)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zscores()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Z-score for each value in the dataset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;outliers()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Z-score based outlier detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Stat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iqrOutliers()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IQR-based outlier detection (box plot whiskers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Install it and start exploring your own data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>php</category>
      <category>statistics</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Programming in the Age of AI: From Code to Intent</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Fri, 13 Feb 2026 22:07:05 +0000</pubDate>
      <link>https://dev.to/robertobutti/programming-in-the-age-of-ai-from-code-to-intent-46eo</link>
      <guid>https://dev.to/robertobutti/programming-in-the-age-of-ai-from-code-to-intent-46eo</guid>
      <description>&lt;p&gt;I was building an AI agent in PHP, wiring up tools, feeding it project documentation, defining how it should interact with the codebase, when a thought stopped me mid-keystroke.&lt;/p&gt;

&lt;p&gt;I was writing instructions for an AI, in a language designed for humans, using conventions invented so human brains could follow the logic.&lt;/p&gt;

&lt;p&gt;And I thought: why are we still doing this?&lt;/p&gt;

&lt;p&gt;Programming languages were designed for us humans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear instructions &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Indentation for our eyes.&lt;/li&gt;
&lt;li&gt;Mnemonics for our memory.&lt;/li&gt;
&lt;li&gt;Clean APIs so we can reason about behavior.&lt;/li&gt;
&lt;li&gt;Design patterns so we don’t forget how systems are structured.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But now AI is writing the code.&lt;/p&gt;

&lt;p&gt;AI doesn’t need mnemonics. It doesn’t get confused by nested brackets. It doesn’t get fatigued by verbosity. It can hold an entire codebase in context.&lt;/p&gt;

&lt;p&gt;So the question feels inevitable: "Should we create a programming language optimized for AI instead of for humans?"&lt;br&gt;
I think the answer is yes.&lt;/p&gt;

&lt;p&gt;But not in the way most people expect.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Pattern We Keep Repeating
&lt;/h2&gt;

&lt;p&gt;Look at the history of how we talk to machines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1950s&lt;/strong&gt;: Humans write machine code. Raw numbers. Only a handful of experts can do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1960s&lt;/strong&gt;: Assembly arrives. Still low-level, but now we see &lt;code&gt;MOV&lt;/code&gt; and &lt;code&gt;ADD&lt;/code&gt; instead of opcodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1980s–90s&lt;/strong&gt;: C, Java, Python. We stop thinking about registers and memory addresses. We write &lt;code&gt;for user in users&lt;/code&gt; and let the compiler handle the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Today&lt;/strong&gt;: We write PHP, Python, TypeScript. AI assists us. But we still write code line by line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the pattern?&lt;/p&gt;

&lt;p&gt;Each generation moves humans further from the machine and closer to pure intent.&lt;/p&gt;

&lt;p&gt;Nobody writes assembly today. Not because it stopped working, it’s still there under everything you use. We just stopped reading it. It became an intermediate representation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code is about to become the next assembly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  The Three Layers of the Future
&lt;/h2&gt;

&lt;p&gt;Here’s what I think is emerging.&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 1: Humans Write Intent
&lt;/h3&gt;

&lt;p&gt;I expect we will not have code, but we will have specifications. Constraints. Business rules. Performance requirements. Security guarantees.&lt;/p&gt;

&lt;p&gt;Instead of:&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We define something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authenticated users can save preferences.
Maximum 100 preferences per user.
Response time under 200ms.
Backwards compatible with the v2 API.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks less like code and more like a contract.&lt;/p&gt;

&lt;p&gt;Programming becomes the act of defining &lt;em&gt;what must be true&lt;/em&gt;, not how to make it true.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: AI Generates Code
&lt;/h3&gt;

&lt;p&gt;PHP, Python, Rust, they don’t disappear.&lt;br&gt;
The ecosystems are too valuable. The runtimes, the libraries, the decades of optimization. AI writes in these languages because that’s where the infrastructure lives.&lt;/p&gt;

&lt;p&gt;But humans read this code less and less.&lt;/p&gt;

&lt;p&gt;Just like you don’t read the assembly output of your C compiler, you won’t read every line of AI-generated PHP.&lt;/p&gt;

&lt;p&gt;Code becomes an implementation detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: Machines Verify Correctness
&lt;/h3&gt;

&lt;p&gt;This is the critical piece, if humans stop reviewing every line, how do we trust the output?&lt;br&gt;
The answer isn’t “trust the AI more.”&lt;/p&gt;

&lt;p&gt;It’s verification.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong type systems&lt;/li&gt;
&lt;li&gt;Contract checking&lt;/li&gt;
&lt;li&gt;Property-based testing&lt;/li&gt;
&lt;li&gt;Static analysis&lt;/li&gt;
&lt;li&gt;Formal verification tools&lt;/li&gt;
&lt;li&gt;Automated proofs that the implementation matches the specification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI-optimized “language” isn’t a new syntax for writing loops.&lt;/p&gt;

&lt;p&gt;It’s a rigorous way to describe intent, and to mathematically or systematically verify that generated code satisfies it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The future of programming isn’t about generating code faster. It’s about proving code correct automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why This Matters Now
&lt;/h2&gt;

&lt;p&gt;We’re in an awkward transition period.&lt;/p&gt;

&lt;p&gt;AI writes code, but humans still review it line by line.&lt;br&gt;
That’s like reviewing assembly output in 1995. Technically possible. Not where the value is.&lt;/p&gt;

&lt;p&gt;The developers who will thrive aren’t the ones who type the fastest.&lt;/p&gt;

&lt;p&gt;They’re the ones who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define precise specifications&lt;/li&gt;
&lt;li&gt;Express constraints clearly&lt;/li&gt;
&lt;li&gt;Build strong verification systems&lt;/li&gt;
&lt;li&gt;Design architectures that are easy to validate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skill is shifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From &lt;em&gt;“How do I implement this algorithm?”&lt;/em&gt; → to &lt;em&gt;“How do I define what correct means?”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;From &lt;em&gt;“Let me debug this code.”&lt;/em&gt; → to &lt;em&gt;“Let me define constraints so bugs can’t exist.”&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;From writing instructions for machines → to writing intent for AI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What This Means for Junior Developers (and People Starting Today)
&lt;/h2&gt;

&lt;p&gt;There’s an uncomfortable question hidden in all this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If AI writes most of the code in the future, is learning programming still worth it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yes. More than ever.&lt;/p&gt;

&lt;p&gt;Understanding programming logic is not about memorizing syntax. It’s about understanding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control flow&lt;/li&gt;
&lt;li&gt;Data structures&lt;/li&gt;
&lt;li&gt;State&lt;/li&gt;
&lt;li&gt;Side effects&lt;/li&gt;
&lt;li&gt;Performance trade-offs&lt;/li&gt;
&lt;li&gt;System design&lt;/li&gt;
&lt;li&gt;Failure modes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you understand how machines think, you can describe intent more precisely.&lt;/p&gt;

&lt;p&gt;You know what the machine expects.&lt;br&gt;
You know what can go wrong.&lt;br&gt;
You know where ambiguity hides.&lt;/p&gt;

&lt;p&gt;And that makes you dramatically more effective when collaborating with AI.&lt;br&gt;
The abstraction layer may rise. But understanding what’s underneath remains a superpower.&lt;/p&gt;

&lt;p&gt;There’s another practical reason this matters today.&lt;/p&gt;

&lt;p&gt;If the future depends on verification and coherence, then the habits we adopt now become critical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use strict typing.&lt;/li&gt;
&lt;li&gt;Write automated tests.&lt;/li&gt;
&lt;li&gt;Use static analysis tools.&lt;/li&gt;
&lt;li&gt;Enforce consistent architecture.&lt;/li&gt;
&lt;li&gt;Keep code uniform and predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because coherent, well-structured codebases are easier for AI to understand, extend, and reason about.&lt;/p&gt;

&lt;p&gt;If tomorrow AI generates 100% of the code, consistency will reduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Errors&lt;/li&gt;
&lt;li&gt;Misunderstandings&lt;/li&gt;
&lt;li&gt;Hidden edge cases&lt;/li&gt;
&lt;li&gt;Bias introduced by unclear patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clean, strictly typed, well-tested systems are not just “good engineering.”&lt;br&gt;
They are AI-ready systems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Learning programming today is not obsolete. It’s training for a higher level of abstraction tomorrow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Real Singularity in Software
&lt;/h2&gt;

&lt;p&gt;The singularity isn’t AI writing code.&lt;br&gt;
That’s already happening.&lt;br&gt;
The real shift is the moment when code becomes an intermediate representation that nobody reads, just like assembly before it.&lt;br&gt;
When that happens, “programming” won’t mean writing PHP or Python.&lt;/p&gt;

&lt;p&gt;It will mean writing intent.&lt;/p&gt;

&lt;p&gt;The language changes. The skill doesn’t.&lt;/p&gt;

&lt;p&gt;It has always been about turning human intent into working machines.&lt;/p&gt;

&lt;p&gt;We’re just removing one more layer of translation.&lt;/p&gt;

&lt;p&gt;If you’re already using AI tools such as Copilot, Claude, Cursor, or others, have you already noticed the shift?&lt;/p&gt;

&lt;p&gt;Less mental energy on syntax. More on defining what you actually want.&lt;/p&gt;

&lt;p&gt;And the uncomfortable question:&lt;/p&gt;

&lt;p&gt;When was the last time you read every line of AI-generated code before shipping it?&lt;/p&gt;

&lt;p&gt;Are we ready to let code become the next assembly?&lt;/p&gt;

&lt;p&gt;Or are we holding on because we’re not ready to trust what we can’t read?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>learning</category>
    </item>
    <item>
      <title>State of JS 2025: Popular Syntax Features Explained with PHP Equivalents</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Sat, 07 Feb 2026 19:15:19 +0000</pubDate>
      <link>https://dev.to/robertobutti/state-of-js-2025-popular-syntax-features-explained-with-php-equivalents-4ffp</link>
      <guid>https://dev.to/robertobutti/state-of-js-2025-popular-syntax-features-explained-with-php-equivalents-4ffp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The State of JS is an annual developer survey that captures how thousands of JavaScript developers feel about the language, its ecosystem, and the features they use most, from syntax and tooling to frameworks and testing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was reading &lt;strong&gt;&lt;a href="https://2025.stateofjs.com/en-US/" rel="noopener noreferrer"&gt;The State of JS 2025&lt;/a&gt;&lt;/strong&gt; survey and landed on the &lt;a href="https://2025.stateofjs.com/en-US/features/#syntax_features" rel="noopener noreferrer"&gt;Syntax Features section&lt;/a&gt;. One question caught my attention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which of these syntax features have you used?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It made me curious. Not just why these features are popular in JavaScript, but also:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do we have something similar in PHP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I turned this into a small personal exercise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand the most-used/appreciated JavaScript syntax features&lt;/li&gt;
&lt;li&gt;Look for their closest equivalents in PHP&lt;/li&gt;
&lt;li&gt;Explain them with some examples&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is the result of that cozy Saturday exploration.&lt;/p&gt;

&lt;p&gt;A JavaScript to PHP walkthrough, written with curiosity rather than comparison wars.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Nullish Coalescing
&lt;/h2&gt;

&lt;p&gt;Nullish coalescing allows you to provide a default value that is used only when a variable is truly missing (&lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Nullish Coalescing operator (&lt;code&gt;??&lt;/code&gt;) provides a default value, but only when the original one is truly missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;someEmpty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;someEmpty&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "default string"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;someEmpty&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt;, so JavaScript falls back to &lt;code&gt;'default string'&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zeroValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;zeroValue&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, &lt;code&gt;0&lt;/code&gt; is a perfectly valid value. Even though it’s falsy, it’s &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;undefined&lt;/code&gt;, so JavaScript keeps it.&lt;/p&gt;

&lt;p&gt;That’s the real power of nullish coalescing: it lets you distinguish between "no value at all" and "a value that just happens to be falsy".&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;PHP introduced the null coalescing operator in PHP 7.0, and it behaves almost identically.&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;$someEmpty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$someEmpty&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'default string'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$foo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "default string"&lt;/span&gt;

&lt;span class="nv"&gt;$zeroValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$baz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$zeroValue&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$baz&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP behaves exactly the same way here. The null coalescing operator follows the same rules and the same mental model as JavaScript, making it immediately familiar if you’re coming from a JS background.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Dynamic Import
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Dynamic imports are especially useful in JavaScript applications because the code itself has to be downloaded. By loading modules only when they are actually needed, dynamic imports help keep the initial download small, improve startup time, and reduce the amount of JavaScript shipped to the browser.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/modules/my-module.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes features like code splitting and lazy loading first‑class citizens in modern frontend development.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent (Conceptual)
&lt;/h3&gt;

&lt;p&gt;PHP applications work very differently. The code runs on the server and does not need to be downloaded by the client, which makes dynamic imports far less critical from a performance perspective.&lt;/p&gt;

&lt;p&gt;PHP loads files synchronously and doesn’t have a promise‑based model like JavaScript, but it still supports composing functionality at runtime:&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="k"&gt;require_once&lt;/span&gt; &lt;span class="k"&gt;__DIR__&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/modules/my-module.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In modern PHP applications, this is usually handled by &lt;strong&gt;autoloading&lt;/strong&gt;. With Composer’s autoloader, classes are loaded automatically when they are referenced without requiring an explicit import via &lt;code&gt;require_once&lt;/code&gt;:&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;$service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App\Service\MyService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this isn’t a direct equivalent of JavaScript’s dynamic &lt;code&gt;import()&lt;/code&gt;, PHP’s autoloading mechanism plays an important role in modern applications. It ensures that classes are loaded only when they are actually used, keeping the codebase modular and avoiding unnecessary includes, even though the underlying performance concerns differ from those in JavaScript.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Private Properties
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Private properties prevents access to class internals from the outside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClassWithPrivateField&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;privateField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;privateField&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to access &lt;code&gt;instance.#privateField&lt;/code&gt; will throw an error.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;PHP has supported private properties for a long time.&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ClassWithPrivateField&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$privateField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getValue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;privateField&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you strong encapsulation that’s enforced at runtime. While JavaScript’s &lt;code&gt;#&lt;/code&gt; syntax is relatively new, the idea itself will feel very familiar to PHP developers who have relied on private properties for years.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Logical Assignment Operators
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Logical assignment operators assign values conditionally using logical operators.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 50&lt;/span&gt;

&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title is empty.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "title is empty"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In JavaScript, the &lt;code&gt;||=&lt;/code&gt; operator assigns a new value only if the left-hand side is falsy. That means values like &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;''&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;, or &lt;code&gt;undefined&lt;/code&gt; will all trigger the assignment. It’s a convenient shorthand when you want to guarantee a usable value, but it’s important to remember that it doesn’t distinguish between “missing” values and intentionally falsy ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;PHP doesn’t offer a direct &lt;code&gt;||=&lt;/code&gt; operator, but the underlying pattern is very familiar. A common way to express it is by reassigning the value using the ternary shortcut &lt;code&gt;?:&lt;/code&gt;.&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;$a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'duration'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'duration'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'duration'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'duration'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since 50 is truthy, PHP keeps the original value.&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;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&gt;'title is empty.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// "title is empty"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the empty string is considered falsy (becasue is a zero length string &lt;code&gt;''&lt;/code&gt;), so PHP falls back to the default.&lt;/p&gt;

&lt;p&gt;It’s worth noting that PHP’s &lt;code&gt;?:&lt;/code&gt; behaves like JS’s &lt;code&gt;||=&lt;/code&gt;, it assigns a new value when the current one is falsy, including &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;''&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;.&lt;br&gt;
If you want behavior closer to JS’s &lt;code&gt;??=&lt;/code&gt;, where only truly missing values trigger the assignment, the null coalescing operator &lt;code&gt;??=&lt;/code&gt; is the right choice.&lt;/p&gt;

&lt;p&gt;Since PHP 7.4 (released in 2019), PHP has had the closest equivalent to JavaScript's &lt;code&gt;??=&lt;/code&gt;: the null coalescing assignment operator &lt;code&gt;??=&lt;/code&gt;. It assigns a value only if the variable is not set or is null, leaving all other values untouched.&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;$a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??=&lt;/span&gt; &lt;span class="s1"&gt;'title is empty.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors JS's &lt;code&gt;??=&lt;/code&gt; behavior almost exactly and is usually the best choice when you want to provide a default only for missing values.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Iterator Methods
&lt;/h2&gt;

&lt;p&gt;Modern JavaScript makes working with sequences of data super convenient thanks to helper methods like &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, and &lt;code&gt;reduce&lt;/code&gt;, which can be applied to arrays — and, in some cases, even iterators.&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const numbers = [1, 2, 3, 4, 5];

// Square the numbers, then keep only the even results, then sum them
const result = numbers
  .map(x =&amp;gt; x * x)
  .filter(x =&amp;gt; x % 2 === 0)
  .reduce((sum, x) =&amp;gt; sum + x, 0);

console.log(result); // 20 -&amp;gt; (4 + 16)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what happens step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.map&lt;/code&gt; squares each number → &lt;code&gt;[1, 4, 9, 16, 25]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.filter&lt;/code&gt; keeps only even numbers → &lt;code&gt;[4, 16]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.reduce&lt;/code&gt; sums them → &lt;code&gt;20&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;While the fluent approach can improve readability, sometimes you can realize that the same logic can be implemented with a single loop, like this:&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;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if you want to find in PHP something similar to the previous JavaScript example, PHP has the functions for map, filter, and reduce an array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$numbers = [1, 2, 3, 4, 5];

// Step 1: square the numbers
$squared = array_map(fn($x) =&amp;gt; $x * $x, $numbers);

// Step 2: filter the even ones
$evenSquares = array_filter($squared, fn($x) =&amp;gt; $x % 2 === 0);

// Step 3: sum them
$result = array_reduce($evenSquares, fn($sum, $x) =&amp;gt; $sum + $x, 0);

echo $result; // 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP 8.5 introduced the pipe operator &lt;code&gt;|&amp;gt;&lt;/code&gt;, so you can chain those functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$numbers = [1, 2, 3, 4, 5];

$result = $numbers
    |&amp;gt; (fn($arr) =&amp;gt; array_map(fn($x) =&amp;gt; $x * $x, $arr))
    |&amp;gt; (fn($arr) =&amp;gt; array_filter($arr, fn($x) =&amp;gt; $x % 2 === 0))
    |&amp;gt; (fn($arr) =&amp;gt; array_reduce($arr, fn($sum, $x) =&amp;gt; $sum + $x, 0));

echo $result; // 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same JavaScript concept, slightly different syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;array_map&lt;/code&gt; = JS &lt;code&gt;.map&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;array_filter&lt;/code&gt; = JS &lt;code&gt;.filter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;array_reduce&lt;/code&gt; = JS &lt;code&gt;.reduce&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pipe operator in PHP 8.5 passes only one value forward. Since &lt;code&gt;array_map&lt;/code&gt;, &lt;code&gt;array_filter&lt;/code&gt;, and &lt;code&gt;array_reduce&lt;/code&gt; take multiple arguments, they need a small wrapper function to fit into the pipeline (&lt;code&gt;fn($arr) =&amp;gt;&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;If you want a fluent approach like JS, you can evaluate using one of the open-source packages (just to say that in PHP, you have a lot of options).&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent with the Open-Source PHP Array Package
&lt;/h3&gt;

&lt;p&gt;Using the &lt;strong&gt;PHP Array Package&lt;/strong&gt; library, we can achieve a similar chainable, fluent syntax in PHP.&lt;/p&gt;

&lt;p&gt;To install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/array
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have installed the package, you can start using it:&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./vendor/autoload.php"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\DataType\Arr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;


&lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arr&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 20 -&amp;gt; (4 + 16)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The PHP Array Package is available here: &lt;a href="https://github.com/Hi-Folks/array" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/array&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This package makes PHP feel almost like JavaScript here, letting you map, filter, and reduce in a single, readable chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Hashbang Grammar
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;Hashbang grammar specifies the interpreter for executable scripts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directove &lt;code&gt;#!/usr/bin/env node&lt;/code&gt; allows JS files to be executed directly from the CLI.&lt;/p&gt;

&lt;p&gt;To make the script executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x script.js
./script.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to specify &lt;code&gt;bun&lt;/code&gt; instead of &lt;code&gt;node&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env bun
&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello world&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Hashbang grammar is technically a shell feature rather than a JavaScript-specific one. It allows a script to declare which executable should run it by specifying the interpreter (such as node) on the first line of the file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;PHP is supported as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;#!/usr/bin/env php
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello world"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. &lt;code&gt;error.cause&lt;/code&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JavaScript
&lt;/h3&gt;

&lt;p&gt;It preserves the original error when rethrowing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EXCEPTION 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EXCEPTION 2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// EXCEPTION 2&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cause&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// EXCEPTION 1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes debugging and error tracing much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP Equivalent
&lt;/h3&gt;

&lt;p&gt;PHP supports &lt;strong&gt;exception chaining&lt;/strong&gt; natively.&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"EXCEPTION 1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"EXCEPTION 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$err&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// EXCEPTION 2&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$err&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPrevious&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;getMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// EXCEPTION 1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can later access the original exception:&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;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getPrevious&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one area where PHP has been ahead for quite some time.&lt;/p&gt;




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

&lt;p&gt;This little exercise wasn’t about proving that one language is better than the other.&lt;/p&gt;

&lt;p&gt;It was about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exploring the most loved JS  features by JavaScript developers (thanks to the survey)&lt;/li&gt;
&lt;li&gt;Appreciating how many of these ideas already exist in PHP&lt;/li&gt;
&lt;li&gt;Seeing how languages keep learning from each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the time, the concepts are the same, only the syntax changes.&lt;/p&gt;

&lt;p&gt;If you enjoyed this kind of cross-language comparison, feel free to share your thoughts or suggest another feature set to explore next. Happy coding! 🚀&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>php</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Laravel &amp; Storyblok: Enabling the Real-Time Visual Editor</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Thu, 05 Feb 2026 21:15:52 +0000</pubDate>
      <link>https://dev.to/robertobutti/laravel-storyblok-enabling-the-real-time-visual-editor-2e0g</link>
      <guid>https://dev.to/robertobutti/laravel-storyblok-enabling-the-real-time-visual-editor-2e0g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This article presents an opinionated yet effective approach to integrating Laravel with Storyblok, focusing on practical decisions that unlock a real-time visual editing experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you think of headless CMS integrations with real-time visual editing, frameworks like React, Vue, or Next.js usually come to mind. But what if I told you that &lt;strong&gt;Laravel with Blade templates&lt;/strong&gt; can deliver the same seamless visual editing experience?&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through how to integrate Storyblok's Visual Editor with Laravel, complete with &lt;strong&gt;real-time preview updates&lt;/strong&gt; and &lt;strong&gt;smart DOM diffing&lt;/strong&gt; to eliminate flickering. No JavaScript framework required.&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%2Flk54vtlyxadvf8chc59r.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%2Flk54vtlyxadvf8chc59r.gif" alt="The Laravel frontend application running in the Storyblok Visual Editor" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Laravel + Storyblok?
&lt;/h2&gt;

&lt;p&gt;Before diving into the code, let's address the elephant in the room: &lt;em&gt;"Isn't Storyblok designed for JavaScript frameworks?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Not at all. Storyblok is &lt;strong&gt;framework-agnostic&lt;/strong&gt;. While the most common documentation emphasizes React and Vue, the underlying APIs work with any technology that can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch JSON from an API&lt;/li&gt;
&lt;li&gt;Render HTML&lt;/li&gt;
&lt;li&gt;Execute JavaScript in the browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Laravel excels at all three. Plus, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server-side rendering&lt;/strong&gt; out of the box (great for SEO and GEO)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blade's elegant templating&lt;/strong&gt; syntax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hydration issues&lt;/strong&gt; (a common pain point with SSR frameworks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler deployment&lt;/strong&gt; compared to Node.js applications (but this is my opinion based on my personal experience, probably if you are a devop engineer with Node skill, you will think differently)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  But here is my opinion. Ready?
&lt;/h2&gt;

&lt;p&gt;There's a reason Laravel keeps winning.&lt;/p&gt;

&lt;p&gt;It's not just about elegant syntax or good documentation. It's about the ecosystem. Laravel has become a complete platform for building scalable, production-grade applications, with first-class packages for nearly everything you need.&lt;/p&gt;

&lt;p&gt;Specifically, for content-driven frontends, you get Blade components that render fast and deploy anywhere. But here's what most developers miss: Laravel is now a serious platform for AI integration. Libraries like Laravel AI and Neuron AI let you build intelligent agents, connect to LLMs, and add AI-powered features without leaving the ecosystem you already know. And with NativePHP, you can ship native desktop and mobile apps using the same Laravel codebase.&lt;/p&gt;

&lt;p&gt;Laravel isn't just for building websites. It's for building channels like kiosks, mobile applications, AI-powered assistants, and everything in between.&lt;/p&gt;

&lt;p&gt;This matters for what we're about to build. A headless CMS integration isn't just about rendering pages. It's about creating a content layer that can feed your website today, your mobile app tomorrow, and your AI agent next month. Laravel handles all of it.&lt;/p&gt;

&lt;p&gt;Now, let's talk about visual editing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's how the integration works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────────┐
│                         Storyblok CMS                            │
│                    ┌────────────────────┐                        │
│                    │   Visual Editor    │                        │
│                    └─────────┬──────────┘                        │
│                       ┌──────┴──────┐                            │
│              Preview URL            Real-time events             │
│                       │             │                            │
└───────────────────────┼─────────────┼────────────────────────────┘
                        │             │
                        ▼             ▼
┌──────────────────────────────────────────────────────────────────┐
│                      Laravel Application                         │
│                                                                  │
│   ┌───────────────────┐         ┌───────────────────────────┐    │
│   │  StoryController  │         │    Storyblok Bridge       │    │
│   │  POST /api/preview│         │    (JavaScript)           │    │
│   └─────────┬─────────┘         └─────────────┬─────────────┘    │
│             │                                 │                  │
│             │        ┌────────────────────────┘                  │
│             │        │  fetch('/api/preview')                    │
│             │        │  + Idiomorph DOM diffing                  │
│             ▼        ▼                                           │
│   ┌───────────────────────────────────────────────────────────┐  │
│   │                    Blade Components                       │  │
│   │                                                           │  │
│   │  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐  │  │
│   │  │hero-section │ │ grid-card   │ │ image (responsive)  │  │  │
│   │  └─────────────┘ └─────────────┘ └─────────────────────┘  │  │
│   │  ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐  │  │
│   │  │  richtext   │ │article-page │ │ newsletter-form     │  │  │
│   │  └─────────────┘ └─────────────┘ └─────────────────────┘  │  │
│   │                                                           │  │
│   └───────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two flows from the Visual Editor:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Preview URL&lt;/strong&gt; → When you open a page in the Visual Editor, Storyblok loads your Laravel app via the configured preview URL. The &lt;code&gt;StoryController&lt;/code&gt; fetches the story and renders it with Blade components.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real-time events&lt;/strong&gt; → As editors make changes, the Storyblok Bridge (JavaScript) receives events, sends the updated story JSON to &lt;code&gt;/api/preview&lt;/code&gt;. The Preview controller renders the json with blade components and the JS receive the updated HTML and uses Idiomorph to update only the changed DOM elements.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Setting up the Laravel project
&lt;/h2&gt;

&lt;p&gt;Start with a fresh Laravel installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;laravel new storyblok-laravel
&lt;span class="nb"&gt;cd &lt;/span&gt;storyblok-laravel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the Storyblok PHP SDK (for Content Delivery API):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require storyblok/php-content-api-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your Storyblok credentials to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STORYBLOK_ACCESS_TOKEN=your_preview_token_here
STORYBLOK_VERSION=draft
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;config/serivces.php&lt;/code&gt; file add the Storyblok configuration:&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="s1"&gt;'storyblok'&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;'access_token'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STORYBLOK_ACCESS_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'version'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'STORYBLOK_VERSION'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Enable HTTPS for local development
&lt;/h2&gt;

&lt;p&gt;Storyblok's Visual Editor requires your preview URL to be served over &lt;strong&gt;HTTPS&lt;/strong&gt;, even during local development. This is a security requirement because the editor runs inside Storyblok's iframe, and modern browsers block mixed content.&lt;/p&gt;

&lt;p&gt;The easiest solution is to use Vite's mkcert plugin with a proxy to your Laravel backend.&lt;/p&gt;

&lt;p&gt;Install the mkcert plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun add &lt;span class="nt"&gt;-d&lt;/span&gt; vite-plugin-mkcert
&lt;span class="c"&gt;# or if you prefer to use npm:&lt;/span&gt;
&lt;span class="c"&gt;# npm install -D vite-plugin-mkcert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your &lt;code&gt;vite.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;laravel&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;laravel-vite-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwindcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tailwindcss/vite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;mkcert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vite-plugin-mkcert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;mkcert&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nf"&gt;laravel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resources/css/app.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resources/js/app.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="nf"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;^(?!(&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;@vite|&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;/resources|&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;/node_modules))&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hmr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;ignored&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;**/storage/framework/views/**&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How this works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;mkcert 127.0.0.1&lt;/code&gt; to create a proper certificate and key for enabling SSL&lt;/li&gt;
&lt;li&gt;Vite serves your frontend at &lt;code&gt;https://127.0.0.1:5173&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The proxy forwards all non-Vite requests to Laravel running at &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You get HTTPS for the Visual Editor + hot module replacement for development&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start both servers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now configure your Storyblok space's &lt;strong&gt;Preview URL&lt;/strong&gt; to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://127.0.0.1:5173/story/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The first time you visit the HTTPS URL, your browser will warn about the self-signed certificate. Click "Advanced" → "Proceed" to accept it. You only need to do this once per browser session.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: Create the Storyblok Service Provider
&lt;/h2&gt;

&lt;p&gt;Register the Storyblok client as a singleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// app/Providers/StoryblokServiceProvider.php
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Storyblok\Api\StoriesApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Storyblok\Api\StoryblokClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoryblokServiceProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ServiceProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Register any application services.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoryblokClient&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="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoryblokClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s2"&gt;"https://api.storyblok.com/v2/cdn/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"services.storyblok.access_token"&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;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoriesApi&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="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoriesApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StoryblokClient&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="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"services.storyblok.version"&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="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Bootstrap any application services.
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: The Story Controller
&lt;/h2&gt;

&lt;p&gt;The controller handles both regular page loads and preview requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// app/Http/Controllers/StoryController.php
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Storyblok\Api\StoriesApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Storyblok\Api\Request\StoryRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoryController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;StoriesApi&lt;/span&gt; &lt;span class="nv"&gt;$storiesApi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"home"&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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;storiesApi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;bySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StoryRequest&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"story"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"story"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;story&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"story"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$story&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"No story provided"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"story"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"story"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$story&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;preview&lt;/code&gt; method is the key to real-time editing. When editors make changes in Storyblok, the Visual Editor sends the updated story JSON to this endpoint, and we return freshly rendered HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Dynamic component rendering
&lt;/h2&gt;

&lt;p&gt;Create a component resolver that maps Storyblok component names to Blade views:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blade&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="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'blok'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
    &lt;span class="nv"&gt;$componentName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'component'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;endphp&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'components.storyblok.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$componentName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dynamic&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"'storyblok.' . &lt;/span&gt;&lt;span class="nv"&gt;$componentName&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;blok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alert alert-warning"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nc"&gt;Component&lt;/span&gt; &lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$componentName&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now creating components is straightforward. Here's an example hero section:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;hero&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blade&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="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'blok'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt; &lt;span class="nc"&gt;\App\Services\StoryblokEditable&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hero min-h-[500px] bg-base-200"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hero-content flex-col lg:flex-row gap-8"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'image'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$blok['image']&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
                &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"(max-width: 640px) 100vw, 384px"&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;widths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"[384, 512, 640, 768]"&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4/3"&lt;/span&gt;
                &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"max-w-sm rounded-lg shadow-2xl"&lt;/span&gt;
                &lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eager"&lt;/span&gt;
                &lt;span class="n"&gt;fetchpriority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;
            &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;

        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"text-5xl font-bold"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'headline'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
                &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"py-6"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;$blok&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;StoryblokEditable::attributes($blok)&lt;/code&gt; call. This adds the &lt;code&gt;data-blok-c&lt;/code&gt; and &lt;code&gt;data-blok-uid&lt;/code&gt; attributes that Storyblok's Visual Editor needs to identify which component you're clicking on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: The Storyblok Bridge (where the magic happens)
&lt;/h2&gt;

&lt;p&gt;Here's the JavaScript that enables real-time preview:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blade&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="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;request&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'_storyblok'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;request&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;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'_storyblok_tk'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://unpkg.com/idiomorph@0.7.4/dist/idiomorph.min.js"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'script'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://app.storyblok.com/f/storyblok-v2-latest.js'&lt;/span&gt;
        &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;storyblokInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;window&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StoryblokBridge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;updatePreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;story&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api/preview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&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;:&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="n"&gt;story&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'text/html'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;newMain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;currentMain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'main'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newMain&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;currentMain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;Idiomorph&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;morph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentMain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newMain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="n"&gt;morphStyle&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'innerHTML'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="n"&gt;ignoreActiveValue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="c1"&gt;// head: { style: 'merge' }&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;console&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Preview error:'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&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="n"&gt;storyblokInstance&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'input'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;story&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;updatePreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;story&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="n"&gt;storyblokInstance&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'published'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'change'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;()&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;window&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&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="n"&gt;document&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})()&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Idiomorph?
&lt;/h3&gt;

&lt;p&gt;The naive approach would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;currentMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this causes &lt;strong&gt;flickering&lt;/strong&gt; because every element gets destroyed and recreated, even if nothing changed. Images reload, animations restart, and form inputs lose focus.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/bigskysoftware/idiomorph" rel="noopener noreferrer"&gt;Idiomorph&lt;/a&gt; (created by the htmx team) performs intelligent DOM diffing. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only updates elements that actually changed&lt;/li&gt;
&lt;li&gt;Preserves focus on form inputs&lt;/li&gt;
&lt;li&gt;Keeps CSS animations running&lt;/li&gt;
&lt;li&gt;Doesn't reload unchanged images&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? Buttery smooth real-time preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: A responsive image component
&lt;/h2&gt;

&lt;p&gt;Storyblok includes a powerful &lt;a href="https://www.storyblok.com/docs/api/image-service" rel="noopener noreferrer"&gt;Image Service&lt;/a&gt; that can resize, crop, and optimize images on the fly. Let's create a component that leverages it:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blade&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="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'sizes'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'100vw'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'widths'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'ratio'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'class'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'loading'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'lazy'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'fetchpriority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'quality'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'smart'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
    &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$alt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'alt'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$focus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'focus'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Build Storyblok Image Service URL
     * @see https://www.storyblok.com/docs/api/image-service
     *
     * Format: {filename}/m/{width}x{height}/smart/filters:{filter1}:{filter2}
     * - /m/ prefix enables WebP conversion
     * - {width}x{height} for resize (use 0 for auto, e.g., 800x0)
     * - /smart for smart cropping (face detection)
     * - /filters:focal(x,y) for focal point cropping
     * - /filters:quality(0-100) for compression
     */&lt;/span&gt;
    &lt;span class="nv"&gt;$buildUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ratio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$focus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$smart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$ratio&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/m/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$dimensions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Add smart cropping if enabled (uses face detection)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$smart&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/smart"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Build filters&lt;/span&gt;
        &lt;span class="nv"&gt;$filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="c1"&gt;// Focal point filter (only applies when cropping, i.e., height &amp;gt; 0)&lt;/span&gt;
        &lt;span class="c1"&gt;// Storyblok focus format: "leftX:leftY:rightX:rightY" or "pointX:pointY"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$focus&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$height&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$smart&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"focal(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$focus&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Quality filter&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$quality&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$quality&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"quality(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$quality&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/filters:"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Build srcset with multiple widths&lt;/span&gt;
    &lt;span class="nv"&gt;$srcset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$widths&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$buildUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;w"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Default src (medium size fallback)&lt;/span&gt;
    &lt;span class="nv"&gt;$defaultWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$widths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$buildUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$defaultWidth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;endphp&lt;/span&gt;

&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;
    &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt;
    &lt;span class="n"&gt;srcset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$srcset&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt;
    &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$sizes&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt;
    &lt;span class="n"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$alt&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$loading&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fetchpriority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fetchpriority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"{{ &lt;/span&gt;&lt;span class="nv"&gt;$fetchpriority&lt;/span&gt;&lt;span class="s2"&gt; }}"&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;
    &lt;span class="n"&gt;decoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"async"&lt;/span&gt;
    &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;endif&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generates a &lt;code&gt;srcset&lt;/code&gt; with multiple resolutions&lt;/li&gt;
&lt;li&gt;Respects Storyblok's focal point for smart cropping&lt;/li&gt;
&lt;li&gt;Applies quality optimization&lt;/li&gt;
&lt;li&gt;Uses lazy loading by default&lt;/li&gt;
&lt;li&gt;Supports &lt;code&gt;fetchpriority="high"&lt;/code&gt; for LCP images&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Hint: once you understand the dynamics of rendering the image via the Storyblok Image Service with PHP, you can evaluate using this package in your project: &lt;a href="https://github.com/storyblok/php-image-service" rel="noopener noreferrer"&gt;https://github.com/storyblok/php-image-service&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8: Rich Text Rendering
&lt;/h2&gt;

&lt;p&gt;Storyblok stores rich text as a JSON document structure. Here's a service to convert it to HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// app/Services/StoryblokRichtext.php
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StoryblokRichtext&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;renderNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;renderNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$content&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;renderNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$child&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'doc'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'paragraph'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'heading'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attrs'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'level'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/h&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attrs'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'level'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'bullet_list'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;ul&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/ul&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'ordered_list'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;ol&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/ol&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'list_item'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/li&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'blockquote'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;blockquote&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/blockquote&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'text'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$content&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;renderText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;e&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'text'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'marks'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$mark&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mark&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s1"&gt;'bold'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/strong&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'italic'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;em&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/em&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'link'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;a href=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$mark&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'attrs'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'href'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/a&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$text&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="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$text&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use the render function in a dedicated component:&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="p"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;storyblok&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;richtext&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blade&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="p"&gt;}}&lt;/span&gt;
&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nf"&gt;props&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!!&lt;/span&gt; &lt;span class="nc"&gt;\App\Services\StoryblokRichtext&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the components where you have the richtext field, you can call the richtext component in this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        @if(!empty($blok['text']))
            &amp;lt;div class="prose prose-lg max-w-none"&amp;gt;
                &amp;lt;x-storyblok.richtext :content="$blok['text']" /&amp;gt;
            &amp;lt;/div&amp;gt;
        @endif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 Hint: once you understand the dynamics of rendering the richtext editor with PHP, you can evaluate using this package in your project: &lt;a href="https://github.com/storyblok/php-tiptap-extension" rel="noopener noreferrer"&gt;https://github.com/storyblok/php-tiptap-extension&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;With this setup, you get:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Real-time visual editing&lt;/strong&gt; - Changes appear instantly as editors type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No flickering&lt;/strong&gt; - Idiomorph ensures smooth updates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click-to-edit&lt;/strong&gt; - Click any component to edit it in Storyblok&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO-friendly&lt;/strong&gt; - Server-rendered HTML, no client-side hydration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast page loads&lt;/strong&gt; - No JavaScript framework overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized images&lt;/strong&gt; - Responsive images with automatic WebP conversion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  An opinionated comparison to React/Vue Implementations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;React/Vue&lt;/th&gt;
&lt;th&gt;Laravel + Blade&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial page load&lt;/td&gt;
&lt;td&gt;Requires hydration&lt;/td&gt;
&lt;td&gt;Instant render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;50-200KB+&lt;/td&gt;
&lt;td&gt;~8KB (idiomorph + bridge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;Requires SSR setup&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Node.js required&lt;/td&gt;
&lt;td&gt;Standard PHP hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Framework-specific&lt;/td&gt;
&lt;td&gt;Familiar Blade syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real-time preview&lt;/td&gt;
&lt;td&gt;Native support&lt;/td&gt;
&lt;td&gt;Works great with bridge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;You don't need React or Vue to build a first-class Storyblok integration. Laravel's Blade templating, combined with a lightweight JavaScript bridge and smart DOM diffing, delivers an excellent visual editing experience.&lt;/p&gt;

&lt;p&gt;The key insights:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Storyblok Bridge is just JavaScript&lt;/strong&gt;: it works in any browser, regardless of your backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-rendered HTML is fine&lt;/strong&gt;: just POST the story JSON and return HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idiomorph eliminates flickering&lt;/strong&gt;: smart diffing makes updates feel native&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blade components map naturally&lt;/strong&gt;: Storyblok's component model fits perfectly with Blade&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Give it a try. Your content editors will love the real-time preview, and you'll love the simplicity of staying in the Laravel ecosystem.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/storyblok/php-content-api-client" rel="noopener noreferrer"&gt;Storyblok PHP SDK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bigskysoftware/idiomorph" rel="noopener noreferrer"&gt;Idiomorph&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.storyblok.com/docs/api/image-service" rel="noopener noreferrer"&gt;Storyblok Image Service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.storyblok.com/docs/packages/storyblok-preview-bridge" rel="noopener noreferrer"&gt;Storyblok Bridge Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have questions? Drop a comment below or find me on &lt;a href="https://twitter.com/rmeetsh" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>storyblok</category>
      <category>headlesscms</category>
      <category>php</category>
    </item>
    <item>
      <title>How to deprecate PHP code without breaking your users</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Sun, 25 Jan 2026 10:39:51 +0000</pubDate>
      <link>https://dev.to/robertobutti/how-to-deprecate-php-code-without-breaking-your-users-4hae</link>
      <guid>https://dev.to/robertobutti/how-to-deprecate-php-code-without-breaking-your-users-4hae</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Removing old code from your project or library without breaking existing users is tricky. This guide shows you how to deprecate PHP methods, classes, and parameters the right way.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you maintain a library, an SDK, or even a shared internal package, you eventually hit this moment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You realize that something you shipped months (or years) ago is no longer the right API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The abstraction may be wrong. The constructor should replace a factory. Maybe a “convenience” method turned out to be a long-term design mistake.&lt;/p&gt;

&lt;p&gt;Whatever the reason, the uncomfortable truth is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Good APIs evolve. And evolution means change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But changing public APIs is dangerous. People build businesses on top of them. Coworkers build systems on top of them. Breaking them casually is how you lose trust.&lt;/p&gt;

&lt;p&gt;Over time, I’ve adopted the same approach used by some open-source PHP projects: &lt;strong&gt;deprecate first, remove later&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is how I do it in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What deprecation actually means
&lt;/h2&gt;

&lt;p&gt;When I mark something as deprecated in my code in my projects, I’m not immediately deleting it. I’m saying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The method still works&lt;/li&gt;
&lt;li&gt;The method is still callable&lt;/li&gt;
&lt;li&gt;The users are warned that they should stop using it&lt;/li&gt;
&lt;li&gt;And I have a proper way to tell users what to use instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deprecation is a promise that defines a migration path. In other words, it is a way to say “This still works today, but it won’t forever. Here’s how to migrate.”&lt;/p&gt;

&lt;p&gt;This gives your users time. It gives you freedom to evolve. And it makes your API predictable and trustworthy.&lt;/p&gt;

&lt;p&gt;This is a good practice, also used in other programming languages. I remember when I used Java (using the &lt;code&gt;@deprecated&lt;/code&gt; tag in the JavaDoc comments) or the "deprecated" decorator in Python. Also, in the PHP world, the Symfony ecosystem uses this practice. If you’ve ever upgraded a Symfony or Laravel app across major versions, you’ve benefited from this discipline, whether you noticed it or not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example from an SDK
&lt;/h2&gt;

&lt;p&gt;Let’s say I originally had an API like this:&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;$client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assetApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, I realize that a more explicit and composable API is better:&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssetApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$managementApiClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don’t want to break existing users. But I do want to push everyone toward the new design.&lt;/p&gt;

&lt;p&gt;So I deprecate the old method.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a properly deprecated method looks like
&lt;/h2&gt;

&lt;p&gt;Here’s the pattern I use (and yes, this is basically what some other PHP open-source projects do internally):&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="cd"&gt;/**
 * @deprecated since 1.1, use new AssetApi($managementApiClient, $spaceId) instead
 * @codeCoverageIgnore
 */&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;assetApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;AssetApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;trigger_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'Method assetApi() is deprecated since 1.1 and will be removed in 2.0. '&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'Use new AssetApi($managementApiClient, $spaceId) instead.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kc"&gt;E_USER_DEPRECATED&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssetApi&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="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This combines three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PHPDoc &lt;code&gt;@deprecated&lt;/code&gt;, IDEs and static analysis tools pick this up immediately&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;trigger_error()&lt;/code&gt; with &lt;code&gt;E_USER_DEPRECATED&lt;/code&gt;, emits a runtime warning so users see it in logs or test output&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@codeCoverageIgnore&lt;/code&gt;, optional, but useful if your deprecated code isn't worth covering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s break down why this is such a powerful pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The &lt;code&gt;@deprecated&lt;/code&gt; PHPDoc tag (for humans and tools)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cd"&gt;/**
 * @deprecated since 1.1, use ...
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This PHPDoc tag is not just documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IDEs will highlight usages&lt;/li&gt;
&lt;li&gt;Static analyzers (PHPStan, Psalm) will warn&lt;/li&gt;
&lt;li&gt;Code reviewers will see it immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is your "development-time" signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Triggering a runtime deprecation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;trigger_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;E_USER_DEPRECATED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users see deprecation warnings in development&lt;/li&gt;
&lt;li&gt;CI can fail on deprecations if configured&lt;/li&gt;
&lt;li&gt;The tests can raise a warning or fail on deprecation according to the test framework configuration&lt;/li&gt;
&lt;li&gt;It makes deprecated usage visible even if nobody reads the docs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Always tell people what to use instead
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'deprecated since 1.1 and will be removed in 2.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AssetApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;instead&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A deprecation without a migration path is just frustration for users.&lt;/p&gt;

&lt;p&gt;A good deprecation message answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is deprecated?&lt;/li&gt;
&lt;li&gt;Since when?&lt;/li&gt;
&lt;li&gt;When will it be removed?&lt;/li&gt;
&lt;li&gt;What should I use instead?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This builds trust and makes upgrades predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The deprecated method becomes a thin compatibility layer
&lt;/h3&gt;

&lt;p&gt;When you deprecate a method, the worst thing you can do is copy its old logic in the new refactored method and keep maintaining it in parallel.&lt;/p&gt;

&lt;p&gt;Instead, whenever possible, the deprecated method should simply delegate to the new implementation.&lt;/p&gt;

&lt;p&gt;In this example, the deprecated method just uses the new initialization logic:&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssetApi&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="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This has several significant benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No duplicated logic to maintain&lt;/li&gt;
&lt;li&gt;No risk of subtle behavioral differences between old and new code&lt;/li&gt;
&lt;li&gt;Any bug fix or improvement automatically applies to both paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, the old API becomes a thin compatibility layer that forwards to the new one.&lt;/p&gt;

&lt;p&gt;This is the strategy used by other PHP open-source projects to preserve backward compatibility without freezing the architecture in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP 8.4+: the native &lt;code&gt;#[Deprecated]&lt;/code&gt; attribute
&lt;/h2&gt;

&lt;p&gt;If your project uses PHP 8.4 or later, you can simplify this using the native &lt;code&gt;#[Deprecated]&lt;/code&gt; attribute:&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="cd"&gt;/**
 * @deprecated since 1.1, use new AssetApi($managementApiClient, $spaceId) instead
 * @codeCoverageIgnore
 */&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Deprecated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'use new AssetApi($managementApiClient, $spaceId) instead'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'1.1'&lt;/span&gt;
&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;assetApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;AssetApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AssetApi&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="nv"&gt;$spaceId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The benefit? No manual &lt;code&gt;trigger_error()&lt;/code&gt;, PHP automatically emits &lt;code&gt;E_USER_DEPRECATED&lt;/code&gt; when the method is called. Cleaner code, same result.&lt;/p&gt;

&lt;p&gt;Keep the PHPDoc &lt;code&gt;@deprecated&lt;/code&gt; tag alongside the attribute for full IDE and documentation support if the IDE you are using does not yet support the PHP &lt;code&gt;Deprecated&lt;/code&gt; attribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHPUnit and deprecations: some practical ways to deal with them
&lt;/h2&gt;

&lt;p&gt;When you run your test suite with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and a deprecated method is executed, PHPUnit will highlight it with a &lt;code&gt;D&lt;/code&gt;, usually in yellow instead of green. That’s PHPUnit telling you: “Something here is using a deprecated API.”&lt;/p&gt;

&lt;p&gt;Sometimes that’s exactly what you want. But depending on your workflow and your goals, you have a few different options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: let PHPUnit show the &lt;code&gt;D&lt;/code&gt; (the default)
&lt;/h3&gt;

&lt;p&gt;The simplest approach is to do nothing.&lt;/p&gt;

&lt;p&gt;Just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/phpunit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and let PHPUnit mark every deprecation with a yellow &lt;code&gt;D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is useful because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deprecations are immediately visible&lt;/li&gt;
&lt;li&gt;You can see if they are increasing over time&lt;/li&gt;
&lt;li&gt;They act as a constant reminder that some migration is still pending&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many projects, this is already good enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: show exactly where deprecations come from
&lt;/h3&gt;

&lt;p&gt;If you want more details about which code is triggering the deprecation, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vendor/bin/phpunit &lt;span class="nt"&gt;--display-deprecations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The exact deprecation messages&lt;/li&gt;
&lt;li&gt;The file and line number&lt;/li&gt;
&lt;li&gt;Which test caused it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is extremely useful when you’re actively cleaning up deprecated usage or during an upgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: fail the test suite on deprecations
&lt;/h3&gt;

&lt;p&gt;If you want to treat any usage of deprecated code as a test failure, PHPUnit can do that for you.&lt;/p&gt;

&lt;p&gt;Enabling the &lt;code&gt;failOnDeprecation&lt;/code&gt; option causes the test suite to fail immediately whenever a deprecation is triggered during the test run. This is a very strict mode, but it’s extremely effective at preventing the accidental use of deprecated APIs.&lt;/p&gt;

&lt;p&gt;To enable it, add &lt;code&gt;failOnDeprecation="true"&lt;/code&gt; to the &lt;code&gt;&amp;lt;phpunit&amp;gt;&lt;/code&gt; element in your &lt;code&gt;phpunit.xml&lt;/code&gt; or &lt;code&gt;phpunit.xml.dist&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8" ?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;phpunit&lt;/span&gt;
    &lt;span class="na"&gt;xmlns:xsi=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema-instance"&lt;/span&gt;
    &lt;span class="na"&gt;xsi:noNamespaceSchemaLocation=&lt;/span&gt;&lt;span class="s"&gt;"https://schema.phpunit.de/12.0/phpunit.xsd"&lt;/span&gt;
    &lt;span class="na"&gt;bootstrap=&lt;/span&gt;&lt;span class="s"&gt;"vendor/autoload.php"&lt;/span&gt;
    &lt;span class="na"&gt;colors=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="na"&gt;cacheDirectory=&lt;/span&gt;&lt;span class="s"&gt;".phpunit.cache"&lt;/span&gt;
    &lt;span class="na"&gt;failOnDeprecation=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a very strong guarantee:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the test suite is green, your code is not using any deprecated APIs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice, this is especially useful for application code or libraries that want to enforce the use of new APIs strictly. For projects that still intentionally test deprecated behavior for backward compatibility, this mode can be too strict, but when you want zero tolerance, it's a great tool.&lt;br&gt;
I use this when my goal is to reduce and eventually remove deprecated code from the codebase. Failing tests serve as a guide, highlighting exactly where deprecated code is still being called.&lt;/p&gt;
&lt;h3&gt;
  
  
  Option 4: remove the tests and ignore coverage for deprecated code
&lt;/h3&gt;

&lt;p&gt;In some cases, especially in libraries and SDKs, tests also serve as documentation.&lt;/p&gt;

&lt;p&gt;I know this approach is a bit opinionated, but I also want to share this option with you.&lt;/p&gt;

&lt;p&gt;If you don’t want deprecated APIs to appear in your examples anymore, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove the tests that exercise the deprecated methods&lt;/li&gt;
&lt;li&gt;Keep only tests for the new API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the deprecated method will naturally show up as uncovered code in your coverage report. If you want to keep coverage clean and honest, you can explicitly mark it as:&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="cd"&gt;/**
 * @codeCoverageIgnore
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is you saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“This code exists only for backward compatibility. It’s not part of the active API anymore.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  There’s no single right choice
&lt;/h3&gt;

&lt;p&gt;Sometimes you want visibility. Sometimes you want strictness. Sometimes you want a clean test suite that only shows the modern API.&lt;/p&gt;

&lt;p&gt;All these approaches are valid; the critical part is to choose one intentionally, instead of letting deprecations become invisible background noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deprecation lifecycle
&lt;/h2&gt;

&lt;p&gt;Over time, I’ve learned that the most essential quality of a good deprecation policy is predictability. Users don’t want surprises, and they definitely don’t want to discover breaking changes by accident.&lt;/p&gt;

&lt;p&gt;That’s why I try to keep the deprecation lifecycle simple, and predictable, for example in my last project I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;1.1&lt;/strong&gt;, the method is introduced as &lt;em&gt;deprecated&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;1.x&lt;/strong&gt;, the method still works, but it emits a warning when used&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;2.0&lt;/strong&gt;, the method is removed entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of timeline gives users plenty of time to adapt. It makes upgrades safer, avoids unpleasant surprises, and lets people plan migrations rather than react to sudden breakages. That’s what makes a library or platform feel reliable and professional over the long term.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I don’t bother deprecating
&lt;/h2&gt;

&lt;p&gt;If a method is new, not public, not documented, and not used by anyone, I just delete it.&lt;/p&gt;

&lt;p&gt;Deprecation is a promise you make to users, and if there are no users, there’s no promise to keep.&lt;/p&gt;

&lt;p&gt;I reserve deprecation for things that are actually part of a contract: public APIs, libraries, frameworks, SDKs, and shared internal platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more than you think
&lt;/h2&gt;

&lt;p&gt;I care a lot about deprecation discipline because it changes the entire dynamic of how a project evolves.&lt;/p&gt;

&lt;p&gt;When deprecations are correctly done, I can improve my APIs without fear, and users can upgrade without panic. The architecture doesn’t get stuck in the past, and the project builds long-term trust simply by being predictable.&lt;/p&gt;

&lt;p&gt;Good deprecation practices are a quiet signal of engineering maturity. They make upgrades boring (and that’s a compliment), they remove drama from releases, and they let the codebase evolve without constantly worrying about breaking everything.&lt;/p&gt;

&lt;p&gt;Bad deprecation practices do the opposite. They create upgrade anxiety, push users into version lock-in, and eventually force big, painful rewrites that nobody enjoys and nobody really has time for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional note: how Symfony manages deprecations at scale
&lt;/h2&gt;

&lt;p&gt;Large frameworks like Symfony face a challenge: as the codebase grows, deprecations multiply, and unmanaged deprecations become noise. Symfony addresses this with the &lt;code&gt;symfony/phpunit-bridge&lt;/code&gt; package, which enhances PHPUnit with deprecation-aware behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captures all &lt;code&gt;E_USER_DEPRECATED&lt;/code&gt; notices during test runs&lt;/li&gt;
&lt;li&gt;Groups and summarizes them for analysis&lt;/li&gt;
&lt;li&gt;Can enforce limits or baselines via the &lt;code&gt;SYMFONY_DEPRECATIONS_HELPER&lt;/code&gt; environment variable (e.g., &lt;code&gt;max[direct]=0&lt;/code&gt; to fail on any new direct deprecation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The guiding principle: existing deprecations are tolerated, but new ones must not slip in unnoticed.&lt;/p&gt;

&lt;p&gt;This turns deprecations into measurable technical debt, the count should shrink over time, never grow silently.&lt;/p&gt;

&lt;p&gt;You don't need the full PHPUnit bridge for most projects. Simply configuring your tests to fail on deprecations (e.g., &lt;code&gt;failOnDeprecation="true"&lt;/code&gt; in PHPUnit 10+) is a practical first step toward disciplined API evolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Deprecation is not about the past. It’s about making the future safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you treat deprecation as a first-class design tool, your codebase can grow for years without collapsing under its own history.&lt;/p&gt;

&lt;p&gt;And that’s what great libraries do.&lt;/p&gt;

</description>
      <category>php</category>
      <category>tutorial</category>
      <category>development</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building Real Agentic AI in PHP with Neuron AI, Gemini, and Tools</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Wed, 14 Jan 2026 18:34:57 +0000</pubDate>
      <link>https://dev.to/robertobutti/building-real-agentic-ai-in-php-with-neuron-ai-gemini-and-tools-2n5</link>
      <guid>https://dev.to/robertobutti/building-real-agentic-ai-in-php-with-neuron-ai-gemini-and-tools-2n5</guid>
      <description>&lt;h2&gt;
  
  
  Build a real tool-using AI agent that understands YouTube videos and answers questions. In pure PHP
&lt;/h2&gt;

&lt;p&gt;For years, using AI in PHP meant little more than sending prompts to an API and displaying the result. Valuable, yes, but also limited, fragile, and prone to hallucinations.&lt;/p&gt;

&lt;p&gt;Today, the industry is moving toward a new paradigm: Agentic AI.&lt;/p&gt;

&lt;p&gt;Instead of a single prompt-response interaction, we now build &lt;strong&gt;agents&lt;/strong&gt;: goal-oriented AI programs that can reason, decide when to use tools, call real code, and operate inside well-defined constraints.&lt;/p&gt;

&lt;p&gt;And yes: PHP is absolutely ready for this.&lt;/p&gt;

&lt;p&gt;Thanks to libraries like &lt;strong&gt;Neuron AI&lt;/strong&gt;, &lt;strong&gt;Symfony AI&lt;/strong&gt;, &lt;strong&gt;Prism&lt;/strong&gt;, and soon &lt;strong&gt;Laravel AI&lt;/strong&gt;, the PHP ecosystem is becoming a first-class platform for building production-grade AI systems.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The PHP ecosystem is becoming a first-class platform for building production-grade AI systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To showcase the power and practicality of this approach, we will build a concrete example: an agent that acts as an assistant, capable of answering questions about a YouTube video. To achieve this, we will give our agent the ability to fetch the video’s transcript (and we will see how easy this is to do in PHP) and then use that transcript as its source of context and knowledge.&lt;/p&gt;

&lt;p&gt;By the end of this article, you will have an agent that you can interrogate about any specific video. Whether you want to summarize a tutorial, extract key insights, or ask precise questions about a long talk or conference recording, you will be able to do it using an agent you built yourself.&lt;/p&gt;

&lt;p&gt;The PHP tools/libraries we are going to use for building the YouTube AI agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neuron AI&lt;/strong&gt; as the PHP agenting framework &lt;a href="https://www.neuron-ai.dev/" rel="noopener noreferrer"&gt;https://www.neuron-ai.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini&lt;/strong&gt; as the LLM provider &lt;a href="https://ai.google.dev/gemini-api/docs" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;gemini-2.5-flash&lt;/code&gt; model, but of course, you can use a richer model &lt;a href="https://ai.google.dev/gemini-api/docs/models" rel="noopener noreferrer"&gt;https://ai.google.dev/gemini-api/docs/models&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The PHP YouTube Transcript open-source library, designed to fetch transcripts for YouTube videos &lt;a href="https://github.com/MrMySQL/youtube-transcript" rel="noopener noreferrer"&gt;https://github.com/MrMySQL/youtube-transcript&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you’ll have an agent that can answer questions about a YouTube video using only its transcript.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an Agent?
&lt;/h2&gt;

&lt;p&gt;An agent is a goal-driven AI program that can reason, plan, and decide when to use tools to complete a task.&lt;/p&gt;

&lt;p&gt;In practice, an agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Has clear instructions&lt;/li&gt;
&lt;li&gt;Has rules and boundaries&lt;/li&gt;
&lt;li&gt;Has tools it can use&lt;/li&gt;
&lt;li&gt;Can call real code&lt;/li&gt;
&lt;li&gt;Can chain multiple steps&lt;/li&gt;
&lt;li&gt;And can operate reliably inside constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the foundation of modern AI applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Tool?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Tool&lt;/strong&gt; is a function that the AI is allowed to call.&lt;/p&gt;

&lt;p&gt;Instead of guessing or hallucinating data, the model can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query your database with your current data&lt;/li&gt;
&lt;li&gt;Call external APIs&lt;/li&gt;
&lt;li&gt;Read files&lt;/li&gt;
&lt;li&gt;Execute business logic&lt;/li&gt;
&lt;li&gt;Fetch external resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User asks a question&lt;/li&gt;
&lt;li&gt;Agent reasons (following predefined steps and instructions)&lt;/li&gt;
&lt;li&gt;Agent decides to call a tool&lt;/li&gt;
&lt;li&gt;Tool executes real PHP code&lt;/li&gt;
&lt;li&gt;Agent receives the result from the Tool&lt;/li&gt;
&lt;li&gt;Agent uses it to answer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is how you build reliable AI systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic AI in the PHP Ecosystem
&lt;/h2&gt;

&lt;p&gt;The PHP ecosystem has always been driven by strong, pragmatic, and highly active communities—and the current wave of AI adoption is no exception. Over the past few months, we’ve seen an increasing number of serious, well-designed libraries emerge, built by people who have a deep understanding of both PHP and real-world production needs.&lt;/p&gt;

&lt;p&gt;Rather than treating AI as a gimmick or a thin API wrapper, these projects are focusing on architecture, maintainability, and integration into existing applications. As a result, the PHP ecosystem is evolving extremely fast to support AI more and more, and the number of available tools is growing almost every week, especially thanks to the energy of the Symfony, Laravel, and independent open-source communities.&lt;/p&gt;

&lt;p&gt;Today, we already have excellent building blocks, such as Neuron AI, which provides a complete agent framework with tool support and orchestration; Symfony AI, which brings first-class AI integration into the Symfony ecosystem; and Prism, which offers a clean abstraction layer over different LLM providers. And this is only the beginning: a dedicated Laravel AI ecosystem is already on the way.&lt;/p&gt;

&lt;p&gt;In this article, we’ll focus on &lt;strong&gt;Neuron AI&lt;/strong&gt;, because it already offers a mature and complete Agent + Tool architecture, making it an ideal foundation for building real-world, production-grade Agentic AI systems in PHP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our goal: a YouTube transcript QA agent
&lt;/h2&gt;

&lt;p&gt;We will build an agent that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts a YouTube video ID&lt;/li&gt;
&lt;li&gt;Uses a specific tool to fetch the transcript (we are going to implement the Agent tool)&lt;/li&gt;
&lt;li&gt;Answers questions using the  transcript&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Installation and dependencies
&lt;/h2&gt;

&lt;p&gt;Before we start building our agent, we need to install a few libraries. Each of them plays a very specific role in the architecture of our application, and together they form a clean, modular, and production-ready stack.&lt;/p&gt;

&lt;p&gt;You can install everything using Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-core/neuron-ai
composer require mrmysql/youtube-transcript
composer require symfony/http-client nyholm/psr7
composer require vlucas/phpdotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s briefly look at why we need each of these packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neuron AI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require neuron-core/neuron-ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the core of our system. &lt;strong&gt;Neuron AI&lt;/strong&gt; provides the Agent abstraction, the Tool system, the orchestration layer, and the provider integrations (such as Gemini, OpenAI, Ollama, etc.). It is the framework that allows us to define instructions, expose tools to the model, and let the agent reason about when and how to use them. Without it, we would just be sending prompts to an API; with it, we are building a real Agentic system.&lt;/p&gt;

&lt;h3&gt;
  
  
  YouTube Transcript fetcher
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require mrmysql/youtube-transcript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This library is a small but very useful utility that allows us to fetch the transcript of a YouTube video programmatically. Instead of scraping HTML or dealing with undocumented APIs ourselves, we can rely on a clean, tested PHP library. This will be the “real-world data source” that our agent will access through a tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symfony HTTP Client and PSR-7 Implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require symfony/http-client nyholm/psr7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The YouTube transcript library relies on PSR-18 (HTTP client) and PSR-7 (HTTP messages) interfaces. Here, we use Symfony’s HTTP client as a robust, production-ready implementation, together with Nyholm’s PSR-7 implementation for request and response objects. This keeps everything standards-based and framework-agnostic.&lt;/p&gt;

&lt;h3&gt;
  
  
  PHP dotenv
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require vlucas/phpdotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we use &lt;strong&gt;phpdotenv&lt;/strong&gt; to load our Gemini API key from a &lt;code&gt;.env&lt;/code&gt; file instead of hardcoding it into the source code. This is a best practice for both security and portability, and it allows us to easily change credentials between environments without touching the code.&lt;/p&gt;




&lt;p&gt;This set of dependencies already shows an important aspect of modern PHP: we are building our AI system using &lt;strong&gt;small, composable, standards-based components&lt;/strong&gt;, rather than a single monolithic library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The complete Agent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;MrMySQL\YoutubeTranscript\TranscriptListFetcher&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Chat\Messages\UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\AIProviderInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Providers\Gemini\Gemini&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\PropertyType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\Tool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;NeuronAI\Tools\ToolProperty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Nyholm\Psr7\Factory\Psr17Factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\HttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\Psr18Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"./vendor/autoload.php"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YouTubeTranscriptQAAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$dotenv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dotenv\Dotenv&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createImmutable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;__DIR__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$dotenv&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GEMINI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"gemini-2.5-flash"&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="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SystemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"You are a helpful assistant that answers questions about a YouTube video using ONLY the provided transcript."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"You must use the tool to retrieve the transcript."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"If the transcript does not contain the answer, say so clearly and ask for another video id."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Never use external knowledge."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"Ask the user for a YouTube video URL or ID if not provided."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Call the get_transcription tool to retrieve the transcript."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Read the transcript carefully."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Answer the user's question strictly using the transcript."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;"Be concise and precise."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"If the transcript does not contain the answer, reply: 'The transcript does not contain this information.'"&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nc"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s2"&gt;"get_transcription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"Retrieve the transcription of a YouTube video."&lt;/span&gt;&lt;span class="p"&gt;,&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;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"video_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PropertyType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"The YouTube video ID (e.g. wT1lcJ_zn18)."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCallable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$video_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$video_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$video_id&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetchTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$videoId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$httpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$psr18Client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Psr18Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$psr17Factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Psr17Factory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$fetcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TranscriptListFetcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$psr18Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$psr17Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$psr17Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$transcriptList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$fetcher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$videoId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$transcriptList&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;findTranscript&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nv"&gt;$language&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

        &lt;span class="nv"&gt;$lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$transcript&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$lines&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;html_entity_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="no"&gt;ENT_QUOTES&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;ENT_HTML5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;"UTF-8"&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="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$lines&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Agent: a guided tour of the code
&lt;/h2&gt;

&lt;p&gt;Now that we’ve seen the complete implementation, let’s walk through the most important parts of the agent step by step. The goal here is not just to understand what the code does, but how the different pieces work together to form a real Agentic AI system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Agent class
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YouTubeTranscriptQAAgent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the heart of the application. By extending the &lt;code&gt;Agent&lt;/code&gt; class provided by Neuron AI, we are not building a simple wrapper around an LLM, we are defining a &lt;strong&gt;full AI agent&lt;/strong&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Its own instructions&lt;/li&gt;
&lt;li&gt;Its own tools&lt;/li&gt;
&lt;li&gt;Its own model provider&lt;/li&gt;
&lt;li&gt;And its own behavior and constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From this point forward, Neuron AI will handle orchestration, tool invocation, and conversation flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Choosing the model provider
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;AIProviderInterface&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method instructs Neuron AI which provider (in this case, via the Gemini class provided by Neuron AI, but you can explore other providers, such as Ollama) and which model should power the agent.&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$_ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GEMINI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"gemini-2.5-flash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we use Gemini as a provider, but one of the strengths of Neuron AI is that this is easily swappable. The rest of the agent does not care whether you are using Gemini, OpenAI, or Ollama, the architecture stays exactly the same.&lt;/p&gt;

&lt;p&gt;We also load the API key from a &lt;code&gt;.env&lt;/code&gt; file, which is a best practice for security and configuration management.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The system prompt: the Agent’s brain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;instructions()&lt;/code&gt; method defines the entire behavioral contract of the agent.&lt;/p&gt;

&lt;p&gt;Instead of a single free-form prompt, we build a structured &lt;code&gt;SystemPrompt&lt;/code&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;background&lt;/code&gt;: what the agent is and what it must never do&lt;/li&gt;
&lt;li&gt;A set of &lt;code&gt;steps&lt;/code&gt;: how to approach the task&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;output&lt;/code&gt; contract: how it should respond&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of the most important parts of the system. These instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Force the agent to use the tool&lt;/li&gt;
&lt;li&gt;Forbid it from using external knowledge&lt;/li&gt;
&lt;li&gt;Greatly reduce hallucinations&lt;/li&gt;
&lt;li&gt;Make the agent more predictable and reliable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, this is where we transform a generic LLM into a specialized, constrained assistant.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Defining the Tool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we declare what the agent is allowed to do in the real world.&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="nc"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"get_transcription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Retrieve the transcription of a YouTube video."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a tool named &lt;code&gt;get_transcription&lt;/code&gt;. The model can decide to call it whenever it thinks it needs the transcript.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Describing the Tool interface
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ToolProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"video_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PropertyType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is crucial: we are not just exposing a PHP function, we are defining a formal schema for it.&lt;/p&gt;

&lt;p&gt;Thanks to this, the model knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What arguments are required&lt;/li&gt;
&lt;li&gt;What type must they have&lt;/li&gt;
&lt;li&gt;How to call the tool correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what makes tool calling reliable and structured, rather than fragile and heuristic.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Connecting the Tool to real PHP code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setCallable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$video_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchTranscript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$video_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the bridge between the AI world and your application.&lt;/p&gt;

&lt;p&gt;When the model decides to call the tool, this PHP function is executed. At this point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The AI stops “imagining”&lt;/li&gt;
&lt;li&gt;And your real, deterministic PHP code takes over&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the exact moment where the agent becomes more than a chatbot.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The actual transcript fetching logic
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetchTranscript&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything here is our old, expected, reliable PHP, and that’s a good thing.&lt;/p&gt;

&lt;p&gt;We use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Symfony’s HTTP client&lt;/li&gt;
&lt;li&gt;PSR-7 / PSR-18 interfaces&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;mrmysql/youtube-transcript&lt;/code&gt; library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To fetch the transcript, normalize it, and return it as a simple text variable.&lt;/p&gt;

&lt;p&gt;From the agent’s point of view, this is just a tool. From your perspective, this provides full control over data access and behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using our new Agent
&lt;/h2&gt;

&lt;p&gt;Once the agent is defined, using it in your application is simple.&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;$agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;YouTubeTranscriptQAAgent&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="nv"&gt;$agent&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"What is the main topic of the video 'wT1lcJ_zn18'?"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We start by instantiating our agent just like any other PHP object. At this point, we are not dealing with a “model client” or a low-level API wrapper, but with a fully configured AI agent that already knows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which model to use&lt;/li&gt;
&lt;li&gt;What its instructions are&lt;/li&gt;
&lt;li&gt;Which tools it is allowed to call&lt;/li&gt;
&lt;li&gt;And how it should behave&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we call the &lt;code&gt;chat()&lt;/code&gt; method, we send a &lt;code&gt;UserMessage&lt;/code&gt; to the agent. This message becomes part of the agent’s conversation context and triggers the whole reasoning process.&lt;/p&gt;

&lt;p&gt;Behind the scenes, Neuron AI orchestrates everything:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent analyzes the user’s question.&lt;/li&gt;
&lt;li&gt;It follows the rules and steps defined in the system prompt.&lt;/li&gt;
&lt;li&gt;It realizes that it needs the video transcript to answer.&lt;/li&gt;
&lt;li&gt;It decides to call the &lt;code&gt;get_transcription&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;Because the  &lt;code&gt;get_transcription&lt;/code&gt; tool requires the video identifier, the video id is extracted from the question (if it is not present or detected, the answer will be a message about the missing video id)&lt;/li&gt;
&lt;li&gt;The Tool PHP code fetches the transcript.&lt;/li&gt;
&lt;li&gt;The agent receives the result (a long text with the transcription) from the Tool and uses it as context.&lt;/li&gt;
&lt;li&gt;Finally, it generates an answer that strictly respects the defined constraints.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The return value of &lt;code&gt;chat()&lt;/code&gt; is a response object. By calling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo $response-&amp;gt;getContent();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We get and print the final answer produced by the agent as a string.&lt;/p&gt;

&lt;p&gt;From the outside, this appears to be a single method call. In reality, it is a full agentic workflow involving reasoning, tool usage, and controlled generation, all hidden behind a clean, idiomatic PHP API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is real Agentic AI
&lt;/h2&gt;

&lt;p&gt;This approach goes far beyond simply sending prompts to a language model. What we have built here is a real agentic system, where the model is no longer a passive text generator but an active component inside a well-defined software architecture.&lt;/p&gt;

&lt;p&gt;The model can autonomously decide when to call a tool, and it can utilize your PHP code as real, concrete capabilities rather than merely as suggestions. At the same time, it operates under strict and explicit rules defined by the system prompt, which constrain its behavior and guide its reasoning.&lt;/p&gt;

&lt;p&gt;Thanks to the carefully designed context, background description, step-by-step instructions, and clearly defined output expectations, the agent is firmly grounded in the task it must perform. This dramatically reduces the risk of hallucinations and makes the whole system far more reliable, predictable, and suitable for real-world applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The PHP ecosystem is clearly entering a new era. What only a short time ago looked like an experimental trend is quickly becoming a solid and mature part of everyday application architecture.&lt;/p&gt;

&lt;p&gt;With tools like Neuron AI, Symfony AI, Prism, and soon Laravel AI, we can now build reliable, production-grade AI agents entirely in PHP, and, more importantly, integrate them naturally into our existing applications and workflows.&lt;/p&gt;

&lt;p&gt;The agent we built in this article is intentionally simple, but it already shows the real power of this approach. From here, you could extend it in many directions: you could add memory to enable reasoning across multiple interactions, introduce multiple tools and let the agent choose between them, connect it to a vector database for semantic search and retrieval-augmented generation, or even orchestrate multiple agents that collaborate on more complex tasks.&lt;/p&gt;

&lt;p&gt;This is not about replacing your application logic with an LLM. It is about augmenting your software with reasoning, decision-making, and controlled autonomy, while keeping PHP firmly in charge of execution and structure.&lt;/p&gt;

&lt;p&gt;Agentic AI is not a distant future.&lt;/p&gt;

&lt;p&gt;It is something you can start building today.&lt;/p&gt;

</description>
      <category>php</category>
      <category>neuronai</category>
      <category>agenticai</category>
      <category>ai</category>
    </item>
    <item>
      <title>Exploring Real-World APIs with DataBlock</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Tue, 06 Jan 2026 19:57:34 +0000</pubDate>
      <link>https://dev.to/robertobutti/exploring-real-world-apis-with-datablock-459i</link>
      <guid>https://dev.to/robertobutti/exploring-real-world-apis-with-datablock-459i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Comparing Symfony and Laravel using GitHub and Packagist data&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the first article "&lt;a href="https://dev.to/robertobutti/handling-nested-php-arrays-using-datablock-26d7"&gt;Handling Nested PHP Arrays Using DataBlock&lt;/a&gt;", we explored DataBlock using a simple in-memory dataset to understand its core ideas: safe access, typed getters, and fluent navigation of nested PHP arrays.&lt;/p&gt;

&lt;p&gt;That was useful to learn the library's methods, but in real projects, data rarely comes from hardcoded arrays.&lt;/p&gt;

&lt;p&gt;More often, it comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;li&gt;JSON or YAML configuration files&lt;/li&gt;
&lt;li&gt;Tooling metadata (like &lt;code&gt;composer.json&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;External services with large, deeply nested responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, we’ll use &lt;strong&gt;real APIs&lt;/strong&gt; to compare Symfony and Laravel and, at the same time, show how DataBlock shines when dealing with &lt;strong&gt;external, untrusted, and deeply nested data&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We’ll use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub API for repository statistics&lt;/li&gt;
&lt;li&gt;Packagist API for download statistics and popular packages&lt;/li&gt;
&lt;li&gt;Raw &lt;code&gt;composer.json&lt;/code&gt; files from GitHub&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And we’ll fetch everything using &lt;strong&gt;Symfony HTTP Client&lt;/strong&gt;, letting DataBlock wrap the responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing DataBlock and the HTTP Client
&lt;/h2&gt;

&lt;p&gt;To follow the examples in this article, you’ll need two packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DataBlock&lt;/strong&gt; itself&lt;/li&gt;
&lt;li&gt;An implementation of &lt;strong&gt;Symfony’s HTTP client&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The PHP DataBlock GitHub repository: &lt;a href="https://github.com/Hi-Folks/data-block" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/data-block&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can install both via Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require hi-folks/data-block symfony/http-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DataBlock is completely framework-agnostic and works with plain PHP projects, Symfony, or any other setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DataBlock uses &lt;code&gt;HttpClientInterface&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In this article, we’ll be calling real APIs. For this, DataBlock provides a convenient helper:&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="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hard-coding a specific HTTP client, this method accepts any implementation of:&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="nc"&gt;Symfony\Contracts\HttpClient\HttpClientInterface&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design has a few important advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can use any compatible HTTP client (Symfony HttpClient, mocked clients, or custom implementations)&lt;/li&gt;
&lt;li&gt;Your code stays testable (you can inject a fake client)&lt;/li&gt;
&lt;li&gt;You stay decoupled from concrete implementations&lt;/li&gt;
&lt;li&gt;DataBlock remains framework-agnostic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our examples, we’ll use the Symfony HTTP Client, but you’re free to swap it with any client that implements the same interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the HTTP Client
&lt;/h2&gt;

&lt;p&gt;Here’s how we’ll create the client used in the rest of the article:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\DataType\Block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\DataType\Enums\Operator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpClient\HttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$httpClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s2"&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="s2"&gt;"User-Agent"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"PHP DataBlock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"Accept"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this client will be passed directly to DataBlock via the &lt;code&gt;fromHttpJsonUrl()&lt;/code&gt; method:&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="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps responsibilities clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The HTTP client handles transport&lt;/li&gt;
&lt;li&gt;DataBlock handles data access and exploration&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fetching real data from GitHub and Packagist
&lt;/h2&gt;

&lt;p&gt;To make this example concrete and enjoyable, we’ll work with real, public data from two widely used ecosystems: Symfony and Laravel.&lt;/p&gt;

&lt;p&gt;We’ll query two different sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub API, to retrieve repository information such as stars, forks, issues, and metadata&lt;/li&gt;
&lt;li&gt;Packagist API, to retrieve download statistics for the main framework packages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More specifically, we’ll use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://api.github.com/repos/symfony/symfony&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://api.github.com/repos/laravel/framework&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And from Packagist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;https://packagist.org/packages/symfony/symfony/stats.json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://packagist.org/packages/laravel/framework/stats.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these endpoints returns a large JSON document with many nested fields, exactly the kind of data that is tedious and error-prone to navigate using plain PHP arrays.&lt;/p&gt;

&lt;p&gt;With DataBlock, we can fetch and wrap these responses in a single step:&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;$symfonyGithub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.github.com/repos/symfony/symfony"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$laravelGithub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://api.github.com/repos/laravel/framework"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$symfonyStats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://packagist.org/packages/symfony/symfony/stats.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$laravelStats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://packagist.org/packages/laravel/framework/stats.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point on, we never touch arrays directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading GitHub stats safely
&lt;/h2&gt;

&lt;p&gt;GitHub responses are large, nested, and can change over time. We don’t want fragile code.&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Symfony GitHub:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Stars: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stargazers_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Forks: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"forks_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Open issues: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"open_issues_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Main language: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$symfonyGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same for Laravel:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Laravel GitHub:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Stars: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"stargazers_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Forks: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"forks_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Open issues: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"open_issues_count"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Main language: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$laravelGithub&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At no point do we need to worry about whether a field exists or not.&lt;br&gt;
 There’s no &lt;code&gt;isset()&lt;/code&gt; scattered around, no PHP warnings to silence, and no manual casting to keep track of. We can focus solely on what we want to extract from the data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading Packagist download statistics
&lt;/h2&gt;

&lt;p&gt;Packagist also returns a fairly rich and nested JSON structure. Even for something that sounds simple like “download counts”, the numbers are not at the top level of the response but grouped under a &lt;code&gt;downloads&lt;/code&gt; object, with multiple levels and fields.&lt;/p&gt;

&lt;p&gt;If you were working with plain arrays, you’d end up writing code like:&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;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'downloads'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'downloads'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'monthly'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'downloads'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'daily'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... plus all the usual &lt;code&gt;isset()&lt;/code&gt; checks to make sure nothing breaks at runtime.&lt;/p&gt;

&lt;p&gt;With DataBlock, you can express exactly what you want to read using dot notation:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Symfony downloads:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.total"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Monthly: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.monthly"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Daily: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.daily"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Laravel downloads:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Total: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.total"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Monthly: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.monthly"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Daily: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelStats&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"downloads.daily"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What’s nice here is that the shape of the JSON almost disappears from the code. You don’t have to care how deeply nested the structure is; you describe the path to the value you want.&lt;/p&gt;

&lt;p&gt;Dot notation turns a deeply nested document into something that feels flat, readable, and easy to reason about, while still remaining safe and type-aware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing composer.json like a data structure
&lt;/h2&gt;

&lt;p&gt;Now let’s fetch the &lt;code&gt;composer.json&lt;/code&gt; files directly from GitHub:&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;$symfonyComposer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://raw.githubusercontent.com/symfony/symfony/master/composer.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$laravelComposer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"https://raw.githubusercontent.com/laravel/framework/master/composer.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;$httpClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting nested typed (strin g) values with &lt;code&gt;getString()&lt;/code&gt;:&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Symfony PHP requirement: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$symfonyComposer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"require.php"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Laravel PHP requirement: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$laravelComposer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"require.php"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exploring dependencies, getting proper blocks data and then accessing to the keys values:&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;$symfonyRequires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$symfonyComposer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$laravelRequires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$laravelComposer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$symfonyRequires&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$laravelRequires&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is structured document exploration, not just array access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtering and sorting real datasets
&lt;/h2&gt;

&lt;p&gt;So far, we’ve only been reading values. But DataBlock can also &lt;strong&gt;work with collections of structured items&lt;/strong&gt;, not just single documents.&lt;/p&gt;

&lt;p&gt;The Packagist endpoint we’re using returns a list of the 100 most popular packages. That means we’re no longer dealing with just a nested object, we’re dealing with a nested dataset.&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;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://packagist.org/explore/popular.json?per_page=100"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$popularPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHttpJsonUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$httpClient&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;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"packages"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, DataBlock provides a lightweight query engine for structured arrays.&lt;/p&gt;

&lt;p&gt;Let’s start by selecting only the packages related to Symfony and Laravel:&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;$symfonyPopularPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$popularPackages&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Operator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LIKE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"symfony"&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;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"favers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$laravelPopularPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$popularPackages&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Operator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LIKE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"laravel"&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;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"favers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s break this down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;where()&lt;/code&gt; Method
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;where()&lt;/code&gt; method filters a dataset based on a condition:&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our case:&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Operator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LIKE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"symfony"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Keep only the items where the &lt;code&gt;name&lt;/code&gt; field contains the word &lt;code&gt;symfony&lt;/code&gt;.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each item in &lt;code&gt;$popularPackages&lt;/code&gt; is itself a structured object (a package entry), and DataBlock applies this condition to all of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;Operator&lt;/code&gt; Enum
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Operator&lt;/code&gt; enum defines how the comparison should be done.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Operator::EQUAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Operator::NOT_EQUAL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Operator::GREATER_THAN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Operator::LESS_THAN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Operator::LIKE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;…and others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, we use:&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="nc"&gt;Operator&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;LIKE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which performs a string contains match.&lt;/p&gt;

&lt;p&gt;Using an enum instead of raw strings makes the API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safer (no typos)&lt;/li&gt;
&lt;li&gt;More explicit&lt;/li&gt;
&lt;li&gt;Easier to discover via autocomplete&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;orderBy()&lt;/code&gt; Method
&lt;/h3&gt;

&lt;p&gt;After filtering, we sort the result:&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"favers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"desc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Sort the remaining items by the &lt;code&gt;favers&lt;/code&gt; field, in descending order.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So now the &lt;strong&gt;most starred / favorited packages&lt;/strong&gt; appear first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inspecting the results
&lt;/h3&gt;

&lt;p&gt;After filtering and sorting the datasets, we don’t just want to know that &lt;em&gt;something&lt;/em&gt; matched; we want to examine the result and understand what we actually obtained.&lt;/p&gt;

&lt;p&gt;Because the result of &lt;code&gt;where()&lt;/code&gt; and &lt;code&gt;orderBy()&lt;/code&gt; is still a &lt;code&gt;Block&lt;/code&gt;, we can keep using all the same tools we’ve seen so far: counting items, accessing nested fields, and safely reading values.&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="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Popular Packages:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Symfony: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$symfonyPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" High Favers: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$symfonyPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;" - "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$symfonyPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0.favers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Laravel: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$laravelPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;" High Favers: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$laravelPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;" - "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="nv"&gt;$laravelPopularPackages&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0.favers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="kc"&gt;PHP_EOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we’ve done quite a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filtered real datasets&lt;/li&gt;
&lt;li&gt;Sorted them by meaningful fields&lt;/li&gt;
&lt;li&gt;Counted and inspected the results&lt;/li&gt;
&lt;li&gt;Navigated deeply nested structures&lt;/li&gt;
&lt;li&gt;And all of this without ever touching raw arrays&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;In this article, we used DataBlock with real HTTP APIs to show that it’s not limited to local arrays or local files. It works just as well with remote JSON and YAML data, such as API responses and external documents.&lt;/p&gt;

&lt;p&gt;Once the data is wrapped in a DataBlock, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access deeply nested values safely&lt;/li&gt;
&lt;li&gt;Filter datasets&lt;/li&gt;
&lt;li&gt;Sort and inspect results&lt;/li&gt;
&lt;li&gt;Extract only the parts you actually care about&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All without worrying about missing keys, broken structures, or manual casting.&lt;/p&gt;

&lt;p&gt;DataBlock doesn’t try to replace DTOs or business logic. Its job is more straightforward and very focused: make structured data, wherever it comes from, easier and safer to explore, query, and consume.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP DataBlock GitHub repository: &lt;a href="https://github.com/Hi-Folks/data-block" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/data-block&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Laravel News mentioned PHP DataBlock in an article: &lt;a href="https://laravel-news.com/data-block" rel="noopener noreferrer"&gt;https://laravel-news.com/data-block&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Previous article about DataBlock: Handling Nested PHP Arrays Using DataBlock &lt;a href="https://dev.to/robertobutti/handling-nested-php-arrays-using-datablock-26d7"&gt;https://dev.to/robertobutti/handling-nested-php-arrays-using-datablock-26d7&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>data</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Handling Nested PHP Arrays Using DataBlock</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Sun, 04 Jan 2026 18:10:29 +0000</pubDate>
      <link>https://dev.to/robertobutti/handling-nested-php-arrays-using-datablock-26d7</link>
      <guid>https://dev.to/robertobutti/handling-nested-php-arrays-using-datablock-26d7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;PHP DataBlock is designed to improve the developer experience when working with complex and deeply nested PHP arrays.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you work with PHP long enough, you will eventually fight an array.&lt;/p&gt;

&lt;p&gt;Not because arrays are bad, but because nested, dynamic arrays are everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API responses&lt;/li&gt;
&lt;li&gt;Configuration files&lt;/li&gt;
&lt;li&gt;Decoded JSON or YAML&lt;/li&gt;
&lt;li&gt;Shared data structures with third-party libraries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And they usually come with the same problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deep &lt;code&gt;isset()&lt;/code&gt; chains&lt;/li&gt;
&lt;li&gt;Missing keys causing subtle bugs&lt;/li&gt;
&lt;li&gt;Manual type casting&lt;/li&gt;
&lt;li&gt;Unclear defaults&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where &lt;strong&gt;DataBlock&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repo &amp;amp; installation&lt;/strong&gt;:&lt;br&gt;
 &lt;a href="https://github.com/Hi-Folks/data-block" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/data-block&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What is DataBlock?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DataBlock&lt;/strong&gt; is a small PHP library that wraps arrays into a safer, more expressive object called a &lt;code&gt;Block&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The DataBlock PHP library doesn’t replace arrays; it improves how you access, read, and handle them.&lt;/p&gt;

&lt;p&gt;With DataBlock, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dot-notation access to nested data&lt;/li&gt;
&lt;li&gt;Typed getters (&lt;code&gt;getInt()&lt;/code&gt;, &lt;code&gt;getString()&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Safe defaults&lt;/li&gt;
&lt;li&gt;Better readability&lt;/li&gt;
&lt;li&gt;Easy data inspection and export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s see why that matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing and using the Block class
&lt;/h2&gt;

&lt;p&gt;To add DataBlock to your projects and start using the Block class with its methods and helpers, you can run the &lt;code&gt;composer require&lt;/code&gt; command:&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="n"&gt;composer&lt;/span&gt; &lt;span class="k"&gt;require&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;folks&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;To support the development, you can "star" ⭐ the repository: &lt;a href="https://github.com/Hi-Folks/data-block" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/data-block&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then, in your PHP files, you can import the &lt;code&gt;HiFolks\DataType\Block&lt;/code&gt; Namespace:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\DataType\Block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The simple example dataset
&lt;/h2&gt;

&lt;p&gt;For the examples included in this article, we are going to use a small dataset representing fruits:&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="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HiFolks\DataType\Block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$fruitsArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"avocado"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Avocado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"fruit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"🥑"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"https://en.wikipedia.org/wiki/Avocado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"rating"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s2"&gt;"apple"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"fruit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"🍎"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"https://en.wikipedia.org/wiki/Apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"rating"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s2"&gt;"banana"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"fruit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"🍌"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"https://en.wikipedia.org/wiki/Banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"yellow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"rating"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;8.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s2"&gt;"cherry"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Cherry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"fruit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"🍒"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"https://en.wikipedia.org/wiki/Cherry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"red"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"rating"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;9&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;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fruitsArray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the array is wrapped in a &lt;code&gt;Block&lt;/code&gt; object, the focus shifts.&lt;br&gt;
Instead of worrying about how the data is structured and whether every key exists, you can concentrate on what you want to read from it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Safe access without &lt;code&gt;isset()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Accessing nested arrays in plain PHP often ends up looking like this:&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;$color&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'avocado'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'color'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but it doesn’t scale very well.&lt;br&gt;
As soon as the data becomes more complex or dynamic, these checks start spreading across the codebase, making it harder to read and maintain.&lt;/p&gt;

&lt;p&gt;With DataBlock, accessing nested values becomes more direct and expressive:&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado.color"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can safely retrieve values using dot notation, even when the structure is deeply nested:&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="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado.color"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado.rating"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"banana.rating"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"apple.notexists"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;string(5) "green"
int(8)
float(8.5)
NULL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing keys are handled gracefully, without triggering warnings or notices.&lt;br&gt;
There’s no need for defensive &lt;code&gt;isset()&lt;/code&gt; checks or fallback logic scattered throughout the code, just clear, predictable access to the data you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Typed getters and defaults
&lt;/h2&gt;

&lt;p&gt;Arrays, by nature, don’t express intent.&lt;br&gt;
When you read a value from an array, it’s not always clear what type that value is supposed to be, or what should happen if it’s missing.&lt;/p&gt;

&lt;p&gt;DataBlock makes this explicit.&lt;/p&gt;

&lt;p&gt;Instead of retrieving a value and then casting or validating it later, DataBlock allows you to express your expectations directly at the point of access:&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="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado.rating"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"banana.rating"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"banana.rating"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"apple.notexists"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"MY_DEFAULT"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&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;int(8)
int(8)
float(8.5)
string(10) "MY_DEFAULT"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the method name clearly communicates the expected type.&lt;br&gt;
A numeric value is automatically cast to an integer or a float, while missing fields can fall back to a sensible default, all in a single, readable expression.&lt;/p&gt;

&lt;p&gt;Why this matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getInt()&lt;/code&gt; and &lt;code&gt;getFloat()&lt;/code&gt; make your intent explicit and self-documenting&lt;/li&gt;
&lt;li&gt;Type casting happens consistently and close to the data source&lt;/li&gt;
&lt;li&gt;Default values are handled inline, without extra conditional logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is especially valuable when working with external or untrusted data, such as API responses, configuration files, or decoded JSON, where types and the presence of fields cannot always be guaranteed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Working with subsets of data
&lt;/h2&gt;

&lt;p&gt;In some cases, you don’t just need a single value; you want to work with an entire section of the dataset.&lt;br&gt;
DataBlock allows you to retrieve a whole branch of the structure in a straightforward way:&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;$avocado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, you receive the underlying array:&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="p"&gt;[&lt;/span&gt;
  &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Avocado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"fruit"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"🥑"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"color"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"green"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s2"&gt;"rating"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when you want to pass the data to existing code that expects a plain PHP array.&lt;/p&gt;

&lt;p&gt;Alternatively, if you want to keep working within the DataBlock API, you can retrieve the same branch as a Block instance:&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;$avocadoBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By using the &lt;code&gt;getBlock()&lt;/code&gt; method, you retain all the benefits of DataBlock, dot notation for nested access, typed getters, and the ability to inspect available keys, while focusing only on the portion of the data you’re interested in.&lt;/p&gt;

&lt;p&gt;This makes it easy to navigate complex structures step by step, without losing context or switching mental models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovering what’s inside your data
&lt;/h2&gt;

&lt;p&gt;When dealing with dynamic or loosely defined data structures, simply knowing what keys are available can make a big difference.&lt;br&gt;
DataBlock provides a simple way to inspect the structure of your dataset by listing its keys:&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"avocado"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"banana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides an immediate overview of the top-level elements, which is especially helpful when working with data from external sources.&lt;/p&gt;

&lt;p&gt;The same approach also works for nested data. You can inspect the keys of a specific branch by retrieving it as a &lt;code&gt;Block&lt;/code&gt;:&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;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado"&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;keys&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"fruit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"wikipedia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being able to explore the available keys at each level makes &lt;code&gt;DataBlock&lt;/code&gt; particularly useful for debugging, data exploration, and building dynamic behaviors such as conditional rendering or automatic mappings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exporting data to YAML (and more)
&lt;/h2&gt;

&lt;p&gt;Beyond reading and navigating data, DataBlock can also help with data representation and serialization.&lt;/p&gt;

&lt;p&gt;In addition to JSON (method &lt;code&gt;toJson()&lt;/code&gt;), DataBlock allows you to serialize your data into YAML, which is often easier to read and reason about, especially for configuration and inspection purposes:&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="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toYaml&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;avocado&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Avocado&lt;/span&gt;
  &lt;span class="na"&gt;fruit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🥑&lt;/span&gt;
  &lt;span class="na"&gt;wikipedia&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://en.wikipedia.org/wiki/Avocado&lt;/span&gt;
  &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;green&lt;/span&gt;
  &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;
&lt;span class="na"&gt;apple&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apple&lt;/span&gt;
  &lt;span class="na"&gt;fruit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;🍎&lt;/span&gt;
  &lt;span class="na"&gt;wikipedia&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://en.wikipedia.org/wiki/Apple&lt;/span&gt;
  &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;red&lt;/span&gt;
  &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Being able to convert a structured dataset into a human-readable or machine-readable format quickly is helpful in many everyday scenarios.&lt;br&gt;
This includes generating configuration files, producing clear debug output, or sharing data with tools and systems that work naturally with YAML.&lt;/p&gt;
&lt;h2&gt;
  
  
  When should you use PHP DataBlock?
&lt;/h2&gt;

&lt;p&gt;DataBlock is a great fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You consume API responses.&lt;/li&gt;
&lt;li&gt;You parse JSON/YAML&lt;/li&gt;
&lt;li&gt;You deal with deeply nested arrays.&lt;/li&gt;
&lt;li&gt;You want safer, clearer PHP code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn’t try to replace domain models; it simply makes raw data easier and safer to work with.&lt;/p&gt;
&lt;h2&gt;
  
  
  Arrays vs DataBlock vs DTOs: which one should you use?
&lt;/h2&gt;

&lt;p&gt;When working with data in PHP, there are usually three common approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Plain PHP arrays&lt;/li&gt;
&lt;li&gt;DataBlock&lt;/li&gt;
&lt;li&gt;DTOs (Data Transfer Objects)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one has a purpose; the key is knowing when to use which.&lt;/p&gt;
&lt;h3&gt;
  
  
  1) Plain PHP Arrays
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The default/first choice.&lt;/strong&gt;&lt;br&gt;
 Arrays are flexible, fast, and built into the language.&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;$rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'avocado'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native PHP feature&lt;/li&gt;
&lt;li&gt;Zero dependencies&lt;/li&gt;
&lt;li&gt;Very flexible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No type safety&lt;/li&gt;
&lt;li&gt;Verbose access for nested data&lt;/li&gt;
&lt;li&gt;Easy to introduce silent bugs&lt;/li&gt;
&lt;li&gt;Hard to understand intent&lt;/li&gt;
&lt;li&gt;Defensive code everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very small scripts&lt;/li&gt;
&lt;li&gt;Simple, flat data&lt;/li&gt;
&lt;li&gt;Performance-critical hot paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once data becomes nested or dynamic, arrays tend to leak complexity everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) DataBlock
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;A safer abstraction over arrays.&lt;/strong&gt;&lt;br&gt;
 DataBlock keeps the flexibility of arrays while adding structure and intent.&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;$rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"avocado.rating"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safe nested access (no warnings)&lt;/li&gt;
&lt;li&gt;Typed getters&lt;/li&gt;
&lt;li&gt;Defaults built in&lt;/li&gt;
&lt;li&gt;Highly readable&lt;/li&gt;
&lt;li&gt;Great for untrusted or dynamic data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still runtime-typed (not compile-time)&lt;/li&gt;
&lt;li&gt;Not a domain model&lt;/li&gt;
&lt;li&gt;Adds a small dependency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API responses&lt;/li&gt;
&lt;li&gt;Configuration files&lt;/li&gt;
&lt;li&gt;JSON / YAML parsing&lt;/li&gt;
&lt;li&gt;Boundary layers (input/output)&lt;/li&gt;
&lt;li&gt;Rapid prototyping without sacrificing safety&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of DataBlock as a smart boundary object between raw data and your application logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) DTOs (Data Transfer Objects)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The most explicit and strict option.&lt;/strong&gt;&lt;br&gt;
 DTOs define exactly what your data looks like.&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="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FruitDTO&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$rating&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong typing&lt;/li&gt;
&lt;li&gt;IDE autocompletion&lt;/li&gt;
&lt;li&gt;Self-documenting&lt;/li&gt;
&lt;li&gt;Great for domain logic&lt;/li&gt;
&lt;li&gt;Easier to refactor safely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verbose&lt;/li&gt;
&lt;li&gt;Requires mapping&lt;/li&gt;
&lt;li&gt;Rigid structure&lt;/li&gt;
&lt;li&gt;Overkill for dynamic data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain models&lt;/li&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Internal application state&lt;/li&gt;
&lt;li&gt;Long-lived data structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DTOs shine inside your application, where structure is stable, and rules matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key difference is where you use them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;My opinionated recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External input (API, JSON, YAML)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DataBlock&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Simple scripts&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Array&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain logic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DTO&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DataBlock&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public interfaces&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;DTO&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Recap: why DataBlock exists
&lt;/h2&gt;

&lt;p&gt;DataBlock doesn’t compete with DTOs; it complements them.&lt;/p&gt;

&lt;p&gt;It lives in the uncomfortable middle ground where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data is messy&lt;/li&gt;
&lt;li&gt;Structure is mostly known&lt;/li&gt;
&lt;li&gt;Safety matters&lt;/li&gt;
&lt;li&gt;Full modeling would be too heavy.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If arrays are too loose and DTOs are too strict, DataBlock is the pragmatic middle layer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;PHP arrays are powerful, but they’re also easy to misuse.&lt;/p&gt;

&lt;p&gt;DataBlock is designed to improve the developer experience when working with complex, nested, and structured datasets by providing a fluent and expressive way to access and handle data.&lt;/p&gt;

&lt;p&gt;Less boilerplate, fewer bugs, more readable code.&lt;/p&gt;

&lt;p&gt;Check it out on GitHub:&lt;br&gt;
 &lt;a href="https://github.com/Hi-Folks/data-block" rel="noopener noreferrer"&gt;https://github.com/Hi-Folks/data-block&lt;/a&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>opensource</category>
      <category>devex</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Essential PHP conferences to attend in 2026</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Thu, 01 Jan 2026 20:25:47 +0000</pubDate>
      <link>https://dev.to/robertobutti/essential-php-conferences-to-attend-in-2026-d11</link>
      <guid>https://dev.to/robertobutti/essential-php-conferences-to-attend-in-2026-d11</guid>
      <description>&lt;p&gt;Here's a curated list of essential developer conferences in &lt;strong&gt;2026&lt;/strong&gt;, focusing on &lt;strong&gt;PHP&lt;/strong&gt; and its major frameworks, including &lt;strong&gt;Laravel&lt;/strong&gt; and &lt;strong&gt;Symfony&lt;/strong&gt;. These events are ideal for staying up-to-date with the ecosystem, sharpening your skills, and connecting with the global PHP community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should I attend a PHP conference in 2026?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2026&lt;/strong&gt; continues the strong momentum of the PHP ecosystem. With modern PHP firmly established, frameworks evolving rapidly, and performance, DX, and tooling improving year after year, it’s an exciting time to be a PHP developer.&lt;/p&gt;

&lt;p&gt;Attending PHP conferences allows you to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stay current with the latest PHP versions, best practices, and architectural patterns&lt;/li&gt;
&lt;li&gt;Learn directly from core contributors, framework maintainers, and industry experts&lt;/li&gt;
&lt;li&gt;Discover new tools, libraries, and workflows that can improve your daily work&lt;/li&gt;
&lt;li&gt;Network with developers from all over the world and grow your professional opportunities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond the talks, conferences are about &lt;strong&gt;people&lt;/strong&gt;. They’re a place to exchange ideas, share challenges, get inspired, and feel part of a welcoming and passionate community.&lt;/p&gt;

&lt;h2&gt;
  
  
  In-Person vs Remote
&lt;/h2&gt;

&lt;p&gt;In-person conferences offer something special: &lt;strong&gt;human connection&lt;/strong&gt;. Meeting fellow developers face-to-face, discussing ideas over coffee, and sharing experiences creates a strong sense of community and belonging. These events help strengthen the PHP community and often lead to long-lasting collaborations and friendships.&lt;/p&gt;

&lt;p&gt;That said, not everyone can travel. Budget, time, or personal constraints can make in-person attendance difficult. Thankfully, many conferences now offer &lt;strong&gt;remote or hybrid options&lt;/strong&gt;, making high-quality content accessible from anywhere.&lt;/p&gt;

&lt;p&gt;In the list below, conferences with a &lt;strong&gt;remote option&lt;/strong&gt; are marked with the 🌐 icon and listed as &lt;em&gt;Online&lt;/em&gt; or &lt;em&gt;Hybrid&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Whether you attend in person or online, the most important thing is staying connected and engaged with the PHP ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  2026 PHP Conferences - The List
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🌐 SymfonyOnline – January 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;January 22 - 23&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Online&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://live.symfony.com/2026-online-january/" rel="noopener noreferrer"&gt;https://live.symfony.com/2026-online-january/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇮🇳 Laracon India 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;January 31 - February 1&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Ahmedabad&lt;/strong&gt;, India&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://laracon.in/" rel="noopener noreferrer"&gt;https://laracon.in/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇳🇱 Laracon EU 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;February 2 - 3&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Amsterdam&lt;/strong&gt;, Netherlands&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://laracon.eu/" rel="noopener noreferrer"&gt;https://laracon.eu/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇬🇧 PHP UK Conference 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;February 20&lt;/strong&gt;,  2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;London&lt;/strong&gt;, UK&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://www.phpconference.co.uk/" rel="noopener noreferrer"&gt;https://www.phpconference.co.uk/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇨🇦 ConFoo 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;February 25 - 27&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Montreal&lt;/strong&gt;, Canada&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://confoo.ca/en/2026/track/php" rel="noopener noreferrer"&gt;https://confoo.ca/en/2026/track/php&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇳🇱 Dutch PHP Conference 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;March 10 - 13&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Amsterdam&lt;/strong&gt;, Netherlands&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://phpconference.nl/" rel="noopener noreferrer"&gt;https://phpconference.nl/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇫🇷 SymfonyLive Paris 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;March 26 - 27&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Paris&lt;/strong&gt;, France&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://live.symfony.com/2026-paris/" rel="noopener noreferrer"&gt;https://live.symfony.com/2026-paris/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇩🇪 SymfonyLive Berlin 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;April 23 - 24&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Berlin&lt;/strong&gt;, Germany&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://live.symfony.com/2026-berlin/" rel="noopener noreferrer"&gt;https://live.symfony.com/2026-berlin/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇮🇹/🌐 phpday 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;May 14 - 15&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Verona&lt;/strong&gt;, Italy (also &lt;strong&gt;Online&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://www.phpday.it/" rel="noopener noreferrer"&gt;https://www.phpday.it/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇺🇸 PHP[TEK] 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;May 19 - 21&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Chicago&lt;/strong&gt; Rosemont, IL, USA&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://phptek.io/" rel="noopener noreferrer"&gt;https://phptek.io/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇯🇵 Laravel Live Japan
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;May 26 - 27&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Tokio&lt;/strong&gt;, Japan&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://laravellive.jp/en" rel="noopener noreferrer"&gt;https://laravellive.jp/en&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇨🇦 SymfonyDay Montreal 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;June 4&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Montreal&lt;/strong&gt;, Canada&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://live.symfony.com/2026-montreal/" rel="noopener noreferrer"&gt;https://live.symfony.com/2026-montreal/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇩🇪/🌐 International PHP Conference 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;June 8 - 12&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Berlin&lt;/strong&gt;, Germany (also &lt;strong&gt;Online&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://phpconference.com/berlin-en/" rel="noopener noreferrer"&gt;https://phpconference.com/berlin-en/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇭🇷 Web Summer Camp 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;July 2 - 4&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Opatija&lt;/strong&gt;, Croatia&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://websummercamp.com/2026" rel="noopener noreferrer"&gt;https://websummercamp.com/2026&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇺🇸 Laracon US 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;July 28 - 29&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Boston&lt;/strong&gt;, MA - USA&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://laracon.us/" rel="noopener noreferrer"&gt;https://laracon.us/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇩🇰 Laravel Live Denmark 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;August 20 - 21&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Copenhagen&lt;/strong&gt;, Denmark&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://laravellive.dk/" rel="noopener noreferrer"&gt;https://laravellive.dk/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇫🇷/🌐 API Platform Conference 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;September 17 - 18&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Lille&lt;/strong&gt;, France (also &lt;strong&gt;Online&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://api-platform.com/con/2026/" rel="noopener noreferrer"&gt;https://api-platform.com/con/2026/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇮🇹 LaravelDay 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;November 18&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Verona&lt;/strong&gt;, Italy&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://www.laravelday.it/" rel="noopener noreferrer"&gt;https://www.laravelday.it/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🇵🇱 SymfonyCon Warsaw 2026
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Date: &lt;strong&gt;November 26 - 27&lt;/strong&gt;, 2026&lt;/li&gt;
&lt;li&gt;Location: &lt;strong&gt;Warsaw&lt;/strong&gt;, Poland&lt;/li&gt;
&lt;li&gt;More info: &lt;a href="https://live.symfony.com/2026-warsaw-con/" rel="noopener noreferrer"&gt;https://live.symfony.com/2026-warsaw-con/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;More events will confirm their dates and locations throughout the year. I’ll keep this list updated as soon as new information becomes available.&lt;/p&gt;

&lt;p&gt;If you know of any great PHP conferences that should be added, feel free to suggest them in the comments.&lt;/p&gt;

&lt;p&gt;And if you’re planning to attend any of these events, online or in person, let me know. Connecting with fellow PHP enthusiasts is always a pleasure!&lt;/p&gt;

&lt;p&gt;Happy coding, and see you at a PHP conference in &lt;strong&gt;2026&lt;/strong&gt; 🚀&lt;/p&gt;

</description>
      <category>php</category>
      <category>techtalks</category>
      <category>laravel</category>
      <category>symfony</category>
    </item>
    <item>
      <title>Why you should always check licenses in your PHP project</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Wed, 24 Dec 2025 14:00:54 +0000</pubDate>
      <link>https://dev.to/robertobutti/why-you-should-always-check-licenses-in-your-php-project-3kon</link>
      <guid>https://dev.to/robertobutti/why-you-should-always-check-licenses-in-your-php-project-3kon</guid>
      <description>&lt;p&gt;When we consider &lt;strong&gt;dependencies&lt;/strong&gt; in a PHP project, we typically focus on security, performance, and maintenance. One critical aspect is often overlooked: software licenses.&lt;/p&gt;

&lt;p&gt;Whether you are building an open-source library, a commercial SaaS, or a private internal application, the licenses of your dependencies matter. Ignoring them can lead to legal risks, compliance issues, and unexpected obligations.&lt;/p&gt;

&lt;p&gt;Composer provides a built-in command to help with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer licenses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this article, we’ll explore why license checks are essential and how to use Composer’s tooling, especially the summary and JSON output formats, to keep your project compliant and under control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why license compliance matters (even for applications)
&lt;/h2&gt;

&lt;p&gt;A common misconception is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’m building an application, not an open source library; licenses don’t matter.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is not true.&lt;/p&gt;

&lt;p&gt;Checking the licenses used in a project is essential for several important reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Legal obligations&lt;/strong&gt; : some licenses (e.g. GPL) may require you to:

&lt;ul&gt;
&lt;li&gt;Disclose source code&lt;/li&gt;
&lt;li&gt;Use the same license for derivative works.&lt;/li&gt;
&lt;li&gt;Provide attribution or license texts.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Commercial restrictions&lt;/strong&gt;: certain licenses are incompatible with proprietary software or paid products.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Future-proofing&lt;/strong&gt;: today’s internal tool might become:

&lt;ul&gt;
&lt;li&gt;An open-source project&lt;/li&gt;
&lt;li&gt;A commercial product&lt;/li&gt;
&lt;li&gt;Part of a larger ecosystem&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;CI/CD &amp;amp; compliance&lt;/strong&gt;: many companies require license audits as part of:

&lt;ul&gt;
&lt;li&gt;Security reviews&lt;/li&gt;
&lt;li&gt;Vendor assessments&lt;/li&gt;
&lt;li&gt;Release pipelines&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Knowing what you depend on is not optional; it’s responsible software development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting an overview with &lt;code&gt;composer licenses -f summary&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The quickest way to understand your project’s license landscape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer licenses -f summary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command generates an output that lists the licenses used, along with the number of packages that use each license. In the output example, we can see that for my current project, the most used license by the dependencies is the license "MIT".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-------------- ------------------------
 License        Number of dependencies
-------------- ------------------------
 MIT            81
 BSD-3-Clause   33
 BSD-2-Clause   2
 GPL-2.0-only   2
 GPL-3.0-only   2
 proprietary    1
 Apache-2.0     1
-------------- ------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This summary gives a clear picture of how licenses are distributed across your dependencies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MIT / BSD / Apache&lt;/strong&gt;
These are generally permissive and safe for most projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPL-2.0-only / GPL-3.0-only&lt;/strong&gt;
Strong copyleft licenses that may impose redistribution obligations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;proprietary&lt;/strong&gt;
A red flag that requires manual review.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are interested in going deeper into Licenses, you can take a look at: &lt;a href="https://opensource.org/licenses" rel="noopener noreferrer"&gt;https://opensource.org/licenses&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The summary  output is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick audits&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Team discussions&lt;/li&gt;
&lt;li&gt;Deciding whether deeper analysis is needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, it doesn’t indicate which packages are responsible for those licenses. For that, we can use the JSON output format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going deeper with &lt;code&gt;composer licenses -f json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To perform automated checks or detailed analysis, Composer can output machine-readable data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composer licenses -f json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON output includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Project metadata (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;dependencies&lt;/code&gt; object where each dependency includes:

&lt;ul&gt;
&lt;li&gt;Package name&lt;/li&gt;
&lt;li&gt;Version&lt;/li&gt;
&lt;li&gt;One or more licenses&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The JSON output follows a structure similar to the one shown below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"laravel/laravel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brick/math"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.14.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"MIT"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"some/gpl-package"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GPL-3.0-only"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This format is ideal for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CI pipelines&lt;/li&gt;
&lt;li&gt;Custom policy enforcement&lt;/li&gt;
&lt;li&gt;Reporting tools&lt;/li&gt;
&lt;li&gt;Automated alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checking license policies with PHP
&lt;/h2&gt;

&lt;p&gt;Once you have JSON output, you can automatically check your organization’s license policy.&lt;/p&gt;

&lt;p&gt;Let’s say your project only allows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MIT;BSD-2-Clause;BSD-3-Clause;Apache-2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using the JSON output, we can write a small PHP script to automatically detect disallowed licenses.&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="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$allowedLicenses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;';'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'MIT;BSD-2-Clause;BSD-3-Clause;Apache-2.0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Execute Composer and capture JSON output&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'composer licenses -f json 2&amp;gt;/dev/null'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exitCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$exitCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;fwrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;STDERR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Failed to execute composer licenses&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Check dependencies&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'dependencies'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$licenses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'license'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;array_intersect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$licenses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowedLicenses&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;"%s (%s) -&amp;gt; %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'version'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s1"&gt;'unknown'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;$licenses&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$licenses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'NO LICENSE'&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scripts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs &lt;code&gt;composer licenses -f json&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;Parses the output safely&lt;/li&gt;
&lt;li&gt;Checks each dependency’s license&lt;/li&gt;
&lt;li&gt;Reports packages that violate your policy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can easily extend this to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fail a CI build&lt;/li&gt;
&lt;li&gt;Generate reports&lt;/li&gt;
&lt;li&gt;Send alerts&lt;/li&gt;
&lt;li&gt;Whitelist or blacklist specific packages&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why checking licenses matters in practice
&lt;/h2&gt;

&lt;p&gt;Automating license checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevents accidental license violations&lt;/li&gt;
&lt;li&gt;Protects your company and clients&lt;/li&gt;
&lt;li&gt;Makes compliance repeatable and auditable&lt;/li&gt;
&lt;li&gt;Encourages better dependency hygiene&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, it shifts license compliance from a manual afterthought to an integrated development practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Composer already gives you the tools, you just need to use them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;composer licenses -f summary&lt;/code&gt; for a quick overview&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;composer licenses -f json&lt;/code&gt; for automation and deep analysis &lt;/li&gt;
&lt;li&gt;Integrate license checks early, not after problems arise&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dependency management is not just about code.&lt;br&gt;
It’s also about &lt;strong&gt;responsibility&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>composer</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to handle API rate limits and HTTP 429 errors in an easy and reliable way</title>
      <dc:creator>Roberto B.</dc:creator>
      <pubDate>Sat, 13 Dec 2025 18:13:54 +0000</pubDate>
      <link>https://dev.to/robertobutti/how-to-handle-api-rate-limits-and-http-429-errors-in-an-easy-and-reliable-way-14e6</link>
      <guid>https://dev.to/robertobutti/how-to-handle-api-rate-limits-and-http-429-errors-in-an-easy-and-reliable-way-14e6</guid>
      <description>&lt;p&gt;API rate limits and HTTP 429 (Too Many Requests) errors are common challenges when building applications that integrate with third-party APIs, as they require multiple requests within a short time window.&lt;/p&gt;

&lt;p&gt;A typical first approach is to handle 429 responses directly in the application logic by adding custom retry and delay mechanisms; however, this often results in complex, fragile, and difficult-to-maintain code.&lt;/p&gt;

&lt;p&gt;When integrating APIs using &lt;strong&gt;Symfony HTTP Client&lt;/strong&gt;, there is no need to reinvent the wheel. Symfony provides a robust, configurable, and production-ready solution through the &lt;strong&gt;RetryableHttpClient&lt;/strong&gt;, allowing you to handle API rate limits automatically and reliably while keeping your code clean and maintainable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Even if you are not using the Symfony Framework and are working with a &lt;strong&gt;standalone PHP script&lt;/strong&gt;, you can still rely on the &lt;strong&gt;Symfony HTTP Client&lt;/strong&gt; component.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Rate limits and 429 HTTP status/errors
&lt;/h2&gt;

&lt;p&gt;When working with third-party APIs, you may occasionally encounter an &lt;strong&gt;HTTP 429 – Too Many Requests&lt;/strong&gt; response. This status code indicates that the client has exceeded the server’s &lt;strong&gt;rate limit&lt;/strong&gt;, typically the number of allowed requests within a specific period (for example, 10 requests per second).&lt;/p&gt;

&lt;p&gt;When a 429 is returned, the API/service is telling the client:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“You are sending requests too fast. Please slow down and try again later.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To successfully complete the request, you should &lt;strong&gt;retry the same API call&lt;/strong&gt; after waiting the amount of time recommended by the server or, if not provided, after a sensible delay such as one second.&lt;/p&gt;

&lt;p&gt;Symfony’s HTTP Client component includes built-in support for retrying failed requests through the &lt;code&gt;RetryableHttpClient&lt;/code&gt; class. This allows developers to automatically retry requests that fail due to rate limiting, network issues, or other recoverable errors.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this article, we will focus on configuring retry mechanisms in Symfony when an HTTP 429 response is received, while keeping in mind that &lt;strong&gt;the same approach can also be applied to other recoverable HTTP errors&lt;/strong&gt;, such as 500-level responses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why implement retries for HTTP 429?
&lt;/h2&gt;

&lt;p&gt;APIs enforce rate limits to protect their infrastructure and ensure fair usage among clients. Exceeding these limits results in a temporary block, which is why a 429 response is returned.&lt;/p&gt;

&lt;p&gt;Automatically retrying a failed request is helpful because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request can succeed shortly after the rate limit resets.&lt;/li&gt;
&lt;li&gt;It improves the resiliency of your application.&lt;/li&gt;
&lt;li&gt;It avoids manually implementing sleep/delay logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many APIs also include a &lt;code&gt;Retry-After&lt;/code&gt; HTTP header indicating how long the client should wait. Symfony’s retry system can detect this and adjust the delay accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using &lt;code&gt;RetryableHttpClient&lt;/code&gt; in Symfony
&lt;/h2&gt;

&lt;p&gt;Symfony provides the &lt;code&gt;RetryableHttpClient&lt;/code&gt; wrapper, which adds automatic retry behavior to an existing HTTP client.&lt;/p&gt;

&lt;p&gt;To enable retries, you must configure a &lt;strong&gt;Retry Strategy&lt;/strong&gt;, which defines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how many times to retry,&lt;/li&gt;
&lt;li&gt;which HTTP status codes trigger a retry (e.g., 429),&lt;/li&gt;
&lt;li&gt;how long to wait between attempts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is a practical example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: retry failed requests after a 429 status
&lt;/h2&gt;

&lt;p&gt;The following example shows how to configure automatic retries for API requests that return an &lt;strong&gt;HTTP 429 (Too Many Requests)&lt;/strong&gt; response using Symfony HTTP Client.&lt;br&gt;
This approach works in both full Symfony applications and standalone PHP scripts, without requiring the Symfony Framework itself.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1: Install the Symfony HTTP Client library
&lt;/h3&gt;

&lt;p&gt;Even if you are not using a Symfony application, you can install the Symfony HTTP Client component via Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require symfony/http-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs only the HTTP Client component and its dependencies.&lt;br&gt;
You do not need to install or use the Symfony Framework to take advantage of its retry and networking features.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Import required classes
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require "./vendor/autoload.php";

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use Symfony\Component\HttpClient\RetryableHttpClient;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 3: Configure a retry strategy
&lt;/h3&gt;

&lt;p&gt;Here we configure a strategy that retries when receiving &lt;strong&gt;HTTP 429&lt;/strong&gt; responses:&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;$retryStrategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GenericRetryStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;statusCodes&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;// Retry on HTTP 429 Too Many Requests&lt;/span&gt;
    &lt;span class="n"&gt;delayMs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// Wait 1 second between retries&lt;/span&gt;
    &lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// Do not increase the delay&lt;/span&gt;
    &lt;span class="n"&gt;maxDelayMs&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// Maximum delay per retry&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This strategy says: "If you receive a 429, retry waiting 1 second between attempts."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Wrap your HTTP client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpClient&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$retryingClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RetryableHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$retryStrategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Make the HTTP request and validate the final response
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$response = $retryingClient-&amp;gt;request(
    "GET",
    "https://dummyjson.com/products/search?q=phone",
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using the &lt;code&gt;RetryableHttpClient&lt;/code&gt;, Symfony will automatically retry the request if a retryable error occurs, such as an &lt;strong&gt;HTTP 429 (Too Many Requests)&lt;/strong&gt;, according to the configured retry strategy.&lt;/p&gt;

&lt;p&gt;However, even after all retry attempts are exhausted, the request may still fail. This can happen, for example, if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the API continues to return 429 responses beyond the maximum number of retries,&lt;/li&gt;
&lt;li&gt;the server responds with a different non-retryable error (such as 400 or 401),&lt;/li&gt;
&lt;li&gt;a network or timeout error occurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this reason, it is crucial to always validate the final response before using its data.&lt;/p&gt;

&lt;p&gt;A safer approach is to check the HTTP status code explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$statusCode = $response-&amp;gt;getStatusCode();

if ($statusCode === 200) {
    $data = $response-&amp;gt;toArray();
    print_r($data);
} else {
    // Handle the error (logging, fallback logic, exception, etc.)
    echo $response-&amp;gt;getStatusCode();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling transport exceptions
&lt;/h2&gt;

&lt;p&gt;In addition to HTTP error responses (such as 429 or 500), API requests may fail due to transport-level errors. These errors occur before a valid HTTP response is received and are typically caused by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network connectivity issues,&lt;/li&gt;
&lt;li&gt;DNS resolution failures,&lt;/li&gt;
&lt;li&gt;timeouts,&lt;/li&gt;
&lt;li&gt;SSL/TLS errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When this happens, Symfony HTTP Client throws transport exceptions, which must be handled explicitly to prevent unexpected application failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Catching Transport Exceptions Safely
&lt;/h3&gt;

&lt;p&gt;Symfony HTTP Client throws exceptions that implement the &lt;code&gt;TransportExceptionInterface&lt;/code&gt;.&lt;br&gt;
You should wrap your request logic in a &lt;code&gt;try/catch&lt;/code&gt; block to handle these cases gracefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;

try {
    $response = $retryingClient-&amp;gt;request(
        "GET",
        "https://dummyjson.com/products/search?q=phone",
    );
    $statusCode = $response-&amp;gt;getStatusCode();

    if ($statusCode === 200) {
        $data = $response-&amp;gt;toArray();
        print_r($data);
    } else {
        // Handle the error (logging, fallback logic, exception, etc.)
        echo $response-&amp;gt;getStatusCode();
    }
} catch (TransportExceptionInterface $e) {
    // Handle transport-level errors (network, timeout, DNS, etc.)
    // e.g. log the error, notify monitoring systems, or retry later
    echo $e-&amp;gt;getMessage();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why validating responses is important
&lt;/h3&gt;

&lt;p&gt;Even with automatic retries enabled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a request can still fail due to network issues,&lt;/li&gt;
&lt;li&gt;retries only apply when a response is actually received,&lt;/li&gt;
&lt;li&gt;transport errors bypass HTTP status codes entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By explicitly catching transport exceptions, you ensure that your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remains stable under adverse network conditions,&lt;/li&gt;
&lt;li&gt;fails gracefully instead of crashing,&lt;/li&gt;
&lt;li&gt;provides meaningful error handling and observability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key takeaway about making the HTTP request
&lt;/h3&gt;

&lt;p&gt;A reliable API integration must handle &lt;strong&gt;both HTTP-level errors and transport-level exceptions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Combining &lt;code&gt;RetryableHttpClient&lt;/code&gt;, response status validation, and proper exception handling gives you a &lt;strong&gt;robust, production-ready solution&lt;/strong&gt; for interacting with rate-limited APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling &lt;code&gt;Retry-After&lt;/code&gt; Header
&lt;/h2&gt;

&lt;p&gt;Many APIs send a &lt;code&gt;Retry-After&lt;/code&gt; header with a value in seconds.&lt;/p&gt;

&lt;p&gt;Symfony’s retry system will automatically prioritize the server-provided delay when available.&lt;/p&gt;

&lt;p&gt;Example server header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 429 Too Many Requests
Retry-After: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, Symfony will wait 2 seconds, even if your strategy says 1 second.&lt;/p&gt;

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

&lt;p&gt;Automatic retries are essential when interacting with rate-limited APIs. Symfony’s &lt;code&gt;RetryableHttpClient&lt;/code&gt; makes this process easy and robust by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;automatically handling 429 (Too Many Requests),&lt;/li&gt;
&lt;li&gt;supporting server-provided retry delays,&lt;/li&gt;
&lt;li&gt;providing configurable retry strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By wrapping your HTTP client with a retry strategy, your application becomes more resilient and better equipped to deal with temporary API throttling.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symfony HTTP Client Documentation&lt;/strong&gt;
Official documentation for the Symfony HTTP Client component, including usage examples and configuration options.
&lt;a href="https://symfony.com/doc/current/http_client.html" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/http_client.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry Failed Requests (Symfony Documentation)&lt;/strong&gt;
Detailed explanation of how to retry failed HTTP requests using &lt;code&gt;RetryableHttpClient&lt;/code&gt; and retry strategies.
&lt;a href="https://symfony.com/doc/current/http_client.html#retry-failed-requests" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/http_client.html#retry-failed-requests&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RetryableHttpClient Class source code&lt;/strong&gt;
Source code of the &lt;code&gt;RetryableHttpClient&lt;/code&gt; class.
&lt;a href="https://github.com/symfony/symfony/blob/8.0/src/Symfony/Component/HttpClient/RetryableHttpClient.php" rel="noopener noreferrer"&gt;https://github.com/symfony/symfony/blob/8.0/src/Symfony/Component/HttpClient/RetryableHttpClient.php&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenericRetryStrategy Class&lt;/strong&gt;
Documentation and source reference for configuring retry logic based on HTTP status codes and delays.
&lt;a href="https://github.com/symfony/symfony/blob/8.0/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php" rel="noopener noreferrer"&gt;https://github.com/symfony/symfony/blob/8.0/src/Symfony/Component/HttpClient/Retry/GenericRetryStrategy.php&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symfony HTTP Client Exceptions&lt;/strong&gt;
Overview of exceptions thrown by the HTTP Client, including transport and HTTP-related exceptions.
&lt;a href="https://symfony.com/doc/current/http_client.html#handling-exceptions" rel="noopener noreferrer"&gt;https://symfony.com/doc/current/http_client.html#handling-exceptions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>symfony</category>
      <category>http</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
