<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged laravel</title>
<link>https://coderlegion.com/tag/laravel</link>
<description>Powered by Question2Answer</description>
<item>
<title>How to Protect a Laravel App with a WAF Using a Placement Checklist</title>
<link>https://coderlegion.com/32042/how-to-protect-a-laravel-app-with-a-waf-using-a-placement-checklist</link>
<description>&lt;p&gt;If you are looking up how to protect a Laravel app with a WAF, split the job before you change DNS. Laravel already owns validation, CSRF, authentication middleware, and authorization. A WAF owns the HTTP path in front of that code. The SafeLine documents used here do not describe a Laravel package, a PHP extension, or a framework connector. This is a go or no-go placement checklist: where the proxy sits, what you leave inside the framework, and which documented controls you enable only after staging. It is not a native integration guide.&lt;/p&gt;
&lt;h3 id=&quot;what-the-sources-actually-support&quot;&gt;What the sources actually support&lt;/h3&gt;
&lt;p&gt;SafeLine is a self-hosted WAF that protects web apps by filtering and monitoring HTTP traffic between a web application and the Internet. The product overview describes it as a reverse proxy. Clients pass through the WAF before they reach the server. It filters, monitors, and blocks malicious HTTP and HTTPS traffic, and it uses policies to decide what is malicious and what is safe. The same page says this is also meant to prevent unauthorized data from leaving the app. The application guide states that SafeLine is developed based on nginx, receives traffic for the original website, cleans malicious attack traffic, and forwards safe and reliable traffic to the original application. Those statements are general. They can cover a Laravel app because Laravel is served over HTTP, not because a Laravel mode is documented. Product overview: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;. Application proxy guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;proxy-record-dns-and-upstream&quot;&gt;Proxy record, DNS, and upstream&lt;/h3&gt;
&lt;p&gt;Confirm public names, ports, and the real app address before you cut over. In the SafeLine web console, the documented flow is to open Applications and add an application. Enter the domain, hostname, or IP of the original app. Enter the port SafeLine will listen on, such as 80 or 443, and check the SSL option for HTTPS apps. Enter the upstream, which is the real address SafeLine will use to forward traffic. Then resolve that domain to the IP of the server where SafeLine runs. If the name still points at the Laravel host, the WAF is not on the request path and none of its policies can act.&lt;/p&gt;
&lt;p&gt;Keep the console and the application listener separate. The deploy guide uses a management port, with 9443 as the console example at &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt;, while the application guide uses the site port such as 80 or 443. Pointing public app DNS at the console port is not the documented way to protect the site.&lt;/p&gt;
&lt;h3 id=&quot;what-stays-inside-laravel&quot;&gt;What stays inside Laravel&lt;/h3&gt;
&lt;p&gt;Form requests, policies, signed URLs, sessions, and CSRF stay in the framework. A reverse proxy does not replace them. What the overview lists as attack coverage includes SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abused, among others. Core capabilities on that page are defenses for web attacks, proactive bot abused defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list.&lt;/p&gt;
&lt;p&gt;Map those capabilities to routes only after a staging pass, and do not invent Laravel-specific rules that the sources do not publish. Rate limiting is documented as throttling traffic that exceeds defined limits, to defend against DoS attacks, brute-force attempts, traffic surges, and other abuse. That can matter for login and password-reset routes, but nothing here says the limiter reads Laravel throttle configuration. Captcha challenge is documented to allow human users and to block crawlers and bots. Authentication challenge, when turned on, requires visitors to enter a password, otherwise they are blocked. Dynamic protection, when turned on, dynamically encrypts HTML and JS on each visit. Test Blade responses, front-end assets, JSON API calls, uploads, and inbound webhooks before you enable those options on production. A web access control list is listed, but these sources do not publish a Laravel webhook exception recipe, so test callbacks instead of assuming an allow entry.&lt;/p&gt;
&lt;h3 id=&quot;host-floor-and-proof&quot;&gt;Host floor and proof&lt;/h3&gt;
&lt;p&gt;Check the host against the deploy guide before cutover: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;. Requirements are Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, and at least 1 CPU core, 1 GB of memory, and 5 GB of disk. The CPU must support SSSE3. The guide states that SafeLine installed on ARM requires a Pro license to work properly and that the Personal Edition is not supported on ARM. It also states that the LTS version will no longer be maintained starting from 9.1.0-LTS on 14 August 2025, and it strongly recommends upgrading to the latest version. Those are infrastructure facts, not Laravel facts. A proxy you cannot run is not protection.&lt;/p&gt;
&lt;p&gt;Prove the path with the tests the application guide already publishes. The sample cases cover SQL injection, XSS, path traversal, code injection, and XXE. Replace the sample host with your staging domain. A blocking page means SafeLine defended that request. Then open the console attack list and click detail. Also send ordinary traffic your app expects: a login, a form post, a static file, and a JSON call. HTTPS apps need the SSL option on the SafeLine listener. How Laravel records the client IP after an extra proxy hop is your application configuration. These sources do not document a Laravel trusted-proxy setting, so do not treat that as a SafeLine feature. If the origin remains reachable on a public address, clients can skip the proxy. Restrict that direct path only after the proxied staging path is proven.&lt;/p&gt;
&lt;p&gt;The product page says SafeLine is production-ready, protecting over 1,000,000 websites and handling over 30,000,000,000 HTTP requests daily. Its published evaluation uses 33,669 samples and lists SafeLine Balance at 71.65 percent detection, 0.07 percent false positive, and 99.45 percent accuracy, and SafeLine Strict at 76.17 percent detection, 0.22 percent false positive, and 99.38 percent accuracy. Use those figures as published results from &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;, not as a guarantee for your routes.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;h4 id=&quot;does-safeline-ship-a-laravel-package-&quot;&gt;Does SafeLine ship a Laravel package?&lt;/h4&gt;
&lt;p&gt;No. The install guide and the application guide describe a reverse proxy and a generic application record with domain, port, and upstream. They do not document an artisan command, a service provider, or a PHP extension. Protection means public traffic reaches SafeLine first and SafeLine forwards cleaned traffic to the Laravel upstream.&lt;/p&gt;
&lt;h4 id=&quot;which-attack-classes-can-i-cite-from-the-docs-&quot;&gt;Which attack classes can I cite from the docs?&lt;/h4&gt;
&lt;p&gt;The overview names SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abused, among others. Replay real application requests as well. A detection list is not a passing test suite for your routes.&lt;/p&gt;
&lt;h4 id=&quot;will-ip-rate-limiting-replace-laravel-auth-&quot;&gt;Will IP rate limiting replace Laravel auth?&lt;/h4&gt;
&lt;p&gt;No. Rate limiting throttles traffic that exceeds defined limits and is described as a defense against DoS attacks, brute-force attempts, traffic surges, and other abuse. Login checks, authorization, and CSRF remain in Laravel. Captcha challenge and authentication challenge are separate documented options. Authentication challenge blocks visitors who do not enter the password.&lt;/p&gt;
&lt;h4 id=&quot;what-fails-the-host-checklist-&quot;&gt;What fails the host checklist?&lt;/h4&gt;
&lt;p&gt;A non-Linux host, an architecture other than x86_64 or arm64, Docker or Docker Compose below the stated versions, less than 1 GB of memory or 5 GB of disk, or a CPU without SSSE3. On ARM, the deploy guide requires a Pro license and states that the Personal Edition is not supported. An x86_64 trial does not prove an ARM host is licensed.&lt;/p&gt;
&lt;h4 id=&quot;how-do-i-know-the-waf-is-actually-in-front-&quot;&gt;How do I know the WAF is actually in front?&lt;/h4&gt;
&lt;p&gt;The application domain must resolve to the SafeLine server. Documented attack samples against that domain should show the blocking page, and the console attack detail should record them. Normal logins, form posts, assets, and JSON calls should still succeed. If the Laravel origin is still open on a public address, the proxy can be bypassed.&lt;/p&gt;
&lt;p&gt;As of September 30, 2026, SafeLine surpassed 700,000 installations worldwide. SafeLine Pro annual promotional pricing is $650-$880 versus the $1,000 regular price.&lt;/p&gt;
&lt;p&gt;You are invited to inspect the project, read the deployment and application guides, and review the promotion before any production DNS change. Stand up a staging hostname, run the published probes, and keep Laravel validation and auth in place while you judge the proxy.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32042/how-to-protect-a-laravel-app-with-a-waf-using-a-placement-checklist</guid>
<pubDate>Thu, 22 Oct 2026 08:02:04 +0000</pubDate>
</item>
<item>
<title>How to Protect a Laravel App with a WAF Cutover Checklist</title>
<link>https://coderlegion.com/32041/how-to-protect-a-laravel-app-with-a-waf-cutover-checklist</link>
<description>&lt;p&gt;A Laravel app does not become protected just because a firewall product exists. The useful question is where the control sits, what traffic it can see, and how you prove the origin still works after the cutover. This guide is a decision checklist for that cutover. It does not promise a Laravel plugin, because the official SafeLine documents do not describe one.&lt;/p&gt;
&lt;p&gt;SafeLine is a self-hosted web application firewall. The home documentation says it protects web apps by filtering and monitoring HTTP traffic between a web application and the Internet, and that a WAF placed in front of an application acts as a reverse-proxy shield: clients pass through the WAF before they reach the server (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The application guide adds that SafeLine is developed based on nginx, receives traffic for the original website, cleans malicious attack traffic, and forwards safe traffic to the original application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). That is the supported way to put it in front of Laravel. Point the proxy at the existing upstream. Do not look for a framework installer that those pages do not provide.&lt;/p&gt;
&lt;h3 id=&quot;what-belongs-in-the-checklist&quot;&gt;What belongs in the checklist&lt;/h3&gt;
&lt;p&gt;Start with the attacks you actually want filtered at the edge. The home page lists SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor activity, brute force, HTTP flood, and bot abuse. Laravel routes that read query strings, form bodies, file paths, XML, or user-supplied URLs can meet several of those classes. Keep parameterized queries, authorization, and output encoding in the application. The outer filter does not retire them.&lt;/p&gt;
&lt;p&gt;Next, choose controls that the product actually documents. Core capabilities on the home page are defenses for web attacks, proactive bot-abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list. The feature list also describes rate limiting for denial-of-service patterns, brute-force attempts, traffic surges, and other abuse; CAPTCHA challenges that allow human users while blocking crawlers and bots; an authentication challenge that requires a password or blocks the visitor; and dynamic protection that encrypts HTML and JS on each visit (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). For a public Laravel site, web-attack blocking and rate limiting are the usual first pass. A password challenge can lock out customers. Dynamic encryption can interact badly with front-end assets, so test it on a copy of the site before you treat it as required.&lt;/p&gt;
&lt;p&gt;Then check the machine, not the framework. The install guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, SSSE3, and a minimum of 1 CPU core, 1 GB of memory, and 5 GB of disk (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). The same guide states that SafeLine on ARM requires a Pro license and that the Personal Edition is not supported on ARM. It also states that the LTS version will no longer be maintained starting from 9.1.0-LTS (14 August 2025), and it strongly recommends that LTS users upgrade to the latest version. If your Laravel host fails any of those checks, fix the host before you edit DNS.&lt;/p&gt;
&lt;h3 id=&quot;how-the-proxy-record-should-look&quot;&gt;How the proxy record should look&lt;/h3&gt;
&lt;p&gt;Log into the web console, open Applications, and add an application. The documented fields are domain, port, and upstream. Domain can be a domain name, hostname, or IP address. Port is what SafeLine listens on, such as 80 or 443. For HTTPS apps, check the SSL option. Upstream is the real address of the original app, and SafeLine forwards traffic there (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). After saving, resolve that domain to the SafeLine server. The Laravel process should remain the upstream, not a second copy of the WAF.&lt;/p&gt;
&lt;p&gt;The console itself is separate from the site you are protecting. After installation, the guide says to open &lt;a rel=&quot;nofollow&quot; href=&quot;https://&amp;lt;safeline-ip&amp;gt;:9443/&quot;&gt;https://&amp;lt;safeline-ip&amp;gt;:9443/&lt;/a&gt; and to obtain the initial administrator account with &lt;code&gt;docker exec safeline-mgt resetadmin&lt;/code&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). Store that password outside the Laravel repository. Automated install is documented as a root-level command, and manual install writes configuration under a directory such as /data/safeline with at least 5 GB free. Those are deployment facts, not Laravel settings.&lt;/p&gt;
&lt;h3 id=&quot;what-to-test-before-you-call-it-protected&quot;&gt;What to test before you call it protected&lt;/h3&gt;
&lt;p&gt;The application guide publishes probe URLs for SQL injection, XSS, path traversal, code injection, and XXE, and tells you to replace the example domain with your own. A blocking page means the defense stopped that request. Open the console attack list and click detail to confirm the event was recorded (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Run those probes only against an application you are allowed to test.&lt;/p&gt;
&lt;p&gt;Then test the traffic you intend to allow. Submit a real login, a validation failure, a form that your users already use, and an API call that should succeed. A WAF that blocks a union-select probe but also blocks a legitimate POST is not finished. The documents do not publish a Laravel route map, so you have to supply that test list yourself.&lt;/p&gt;
&lt;p&gt;Read published evaluation numbers as published numbers, then repeat the check on your own app. On 33,669 samples, the home page reports SafeLine Balance at 71.65 percent detection, 0.07 percent false positives, and 99.45 percent accuracy, and SafeLine Strict at 76.17 percent detection, 0.22 percent false positives, and 99.38 percent accuracy (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The same section says SafeLine is production-ready, with over 180,000 installations worldwide, protecting over 1,000,000 websites and handling over 30,000,000,000 HTTP requests daily. As of September 30, 2026, SafeLine surpassed 700,000 installations worldwide. SafeLine Pro annual promotional pricing is $650-$880 versus the $1,000 regular price. Neither the sample table nor the price range replaces a cutover test on your Laravel upstream.&lt;/p&gt;
&lt;p&gt;If you later remove the proxy, the install guide says to enter the SafeLine directory, run &lt;code&gt;docker compose down&lt;/code&gt;, and remove that directory only when you are sure you no longer want the data (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). Do that only after DNS no longer points clients at a stopped proxy.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-include-a-laravel-plugin-or-middleware-&quot;&gt;Does SafeLine include a Laravel plugin or middleware?&lt;/h4&gt;
&lt;p&gt;No. The install and application documents describe a reverse proxy, a console, and an application record with domain, port, and upstream. They do not document a Laravel package. Protection means proxying the existing app, not importing a framework module.&lt;/p&gt;
&lt;h4 id=&quot;can-application-validation-be-removed-after-the-waf-is-live-&quot;&gt;Can application validation be removed after the WAF is live?&lt;/h4&gt;
&lt;p&gt;No. SafeLine filters and monitors HTTP traffic before the origin sees it. Laravel still needs authorization, validation, and safe database access. Treat the WAF as an outer layer in the checklist, not as a replacement for application controls.&lt;/p&gt;
&lt;h4 id=&quot;which-documented-controls-should-i-enable-first-&quot;&gt;Which documented controls should I enable first?&lt;/h4&gt;
&lt;p&gt;Use documented web-attack defenses first, and consider IP-based rate limiting if brute force, HTTP floods, or traffic surges are in scope. Add a CAPTCHA challenge when bot abuse is a real problem. Test the authentication challenge and HTML or JS dynamic protection before enabling them on a public Laravel site, because a password wall or encrypted page output can break ordinary visitors.&lt;/p&gt;
&lt;h4 id=&quot;what-proves-the-cutover-worked-&quot;&gt;What proves the cutover worked?&lt;/h4&gt;
&lt;p&gt;Legitimate users and API clients still reach the upstream, HTTPS works if you checked the SSL option, and the documented attack probes against your own domain produce a block page that appears in the attack detail view. DNS should point at the SafeLine server only after those checks pass.&lt;/p&gt;
&lt;h4 id=&quot;is-the-minimum-server-size-enough-for-every-laravel-app-&quot;&gt;Is the minimum server size enough for every Laravel app?&lt;/h4&gt;
&lt;p&gt;The documented minimum is 1 core, 1 GB of memory, and 5 GB of disk, with Docker and SSSE3. That is a floor for installation, not a capacity guarantee. On ARM, the install guide says a Pro license is required and the Personal Edition is not supported.&lt;/p&gt;
&lt;p&gt;If this checklist matches the way you want to protect the app, read the project and the official docs, then rehearse the proxy on a non-production name before you move real users. You are invited to compare the documented controls with your own routes and to keep the upstream test list next to the change. Official project: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt; Official documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt; Install guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt; Application guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt; Promotion: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32041/how-to-protect-a-laravel-app-with-a-waf-cutover-checklist</guid>
<pubDate>Thu, 22 Oct 2026 08:01:33 +0000</pubDate>
</item>
<item>
<title>A Clean Scan Is a Data Point, Not a Verdict: Lessons from PHP2Shell Week</title>
<link>https://coderlegion.com/32029/a-clean-scan-is-a-data-point-not-a-verdict-lessons-from-php2shell-week</link>
<description>&lt;p&gt;PHP's two critical flaws, CVE-2026-109618 and CVE-2026-109619 (PHP2Shell), were patched in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. The advice everywhere was to update. I want to write about something less covered: how much you can honestly claim afterwards.&lt;/p&gt;
&lt;h4 id=&quot;the-scan-that-found-nothing&quot;&gt;The scan that found nothing&lt;/h4&gt;
&lt;p&gt;I ran &lt;a rel=&quot;nofollow&quot; href=&quot;http://github.com/hryagstn/laravel-scalpel&quot;&gt;Laravel Scalpel&lt;/a&gt;, an open source filesystem intrusion scanner, against three Laravel applications after updating. No backdoor files. My first instinct was relief. My second was to ask what that result actually rules out.&lt;/p&gt;
&lt;h4 id=&quot;why-a-negative-result-is-weak-evidence&quot;&gt;Why a negative result is weak evidence&lt;/h4&gt;
&lt;p&gt;A scan can only report on what it can see, and it can only recognise what it knows how to recognise. Scalpel looks at five things: PHP files in places they should not be, known obfuscation patterns, risky directives in &lt;code&gt;.htaccess&lt;/code&gt; and &lt;code&gt;.user.ini&lt;/code&gt;, the integrity of &lt;code&gt;.env&lt;/code&gt;, and differences from a saved baseline. Each is a question it answers well. Anything outside them is a question it never asks. Coverage and sensitivity multiply, and if either is low, 'found nothing' tells you little.&lt;/p&gt;
&lt;p&gt;There are at least two explanations for my result. My applications are small, which lowers the odds that anything needed to hide. Or something sits outside the questions above. A scanner inside the application also shares the web server's user, so a flaw that grants code execution in that process gives an attacker the means to tamper with the tool. I ran Scalpel from inside the application and through an external runner for that reason, which narrows the gap without closing it. My strongest evidence is not the scan but a routine that predates it: since April, a scheduled external diff against my baseline has reported to a Telegram bot. I refreshed the baseline myself after the update, so any unexpected change before that should already have alerted me, and none did. That evidence has conditions too: the runs must actually have happened in the window, and the alert level must cover what an attacker would change. Scalpel's documentation states the first limit plainly, and I would rather state all of them than have a reader discover them. From the output alone I cannot separate these cases.&lt;/p&gt;
&lt;h4 id=&quot;a-ladder-of-claims&quot;&gt;A ladder of claims&lt;/h4&gt;
&lt;p&gt;It helped me to treat each check as supporting a different, specific statement:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Version verified:&lt;/strong&gt; the PHP that serves web traffic is a fixed version, containers included. Supports: 'this flaw can no longer be used against us.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logs reviewed from 20 October:&lt;/strong&gt; no repeated 500s or memory limit errors that look like probing. Supports: 'we saw no sign of attempts in these logs.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Filesystem scan clean:&lt;/strong&gt; no known backdoor patterns in the application files. Supports: 'we found no evidence of intrusion in these files.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets rotated:&lt;/strong&gt; exposed credentials no longer work. Supports: 'even if memory was read, those secrets are dead.'&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rebuilt from a known good image:&lt;/strong&gt; supports the only claim close to 'clean.'&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most of us stop at rungs one to three and announce we are fine. Only the last rung justifies that word.&lt;/p&gt;
&lt;h4 id=&quot;why-the-timing-makes-this-sharper&quot;&gt;Why the timing makes this sharper&lt;/h4&gt;
&lt;p&gt;The identifiers leaked about a day before the fix, and a security firm reported exploitation attempts shortly after release. For teams that patched hours or days later, the unprotected window was real, and a patch does nothing about what happened inside it.&lt;/p&gt;
&lt;h4 id=&quot;what-i-would-write-in-a-post-patch-note&quot;&gt;What I would write in a post-patch note&lt;/h4&gt;
&lt;p&gt;Something like: 'Updated PHP on this date. Reviewed logs from 20 October. Scanned the application filesystem. Found no evidence of intrusion in those places. Rotated these secrets.' Each sentence is a claim you can defend, and the places you did not check are visible by omission.&lt;/p&gt;
&lt;p&gt;I would be glad to hear what steps other teams include in their own post-patch verification.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32029/a-clean-scan-is-a-data-point-not-a-verdict-lessons-from-php2shell-week</guid>
<pubDate>Thu, 22 Oct 2026 05:13:41 +0000</pubDate>
</item>
<item>
<title>Stripe Webhooks in Laravel: Verify, Dedupe and Retry Safely</title>
<link>https://coderlegion.com/31895/stripe-webhooks-in-laravel-verify-dedupe-and-retry-safely</link>
<description>&lt;p&gt;If you take payments with Stripe, the money moving is only half the story. The other half arrives later as &lt;strong&gt;Stripe webhooks&lt;/strong&gt;: messages that tell your app a payment succeeded, failed, was refunded or disputed. Handle them badly and you get customers who paid but have no access, double-processed orders, or a billing status that quietly drifts away from reality.&lt;/p&gt;
&lt;p&gt;I build these integrations as part of &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/services/api-development-integrations&quot;&gt;API development and integrations&lt;/a&gt; work. This guide covers how to receive Stripe webhooks safely in Laravel: verifying them, ignoring duplicates, responding fast and recovering when something fails.&lt;/p&gt;
&lt;h3 id=&quot;why-webhooks-not-just-the-redirect-after-checkout&quot;&gt;Why webhooks, not just the redirect after checkout&lt;/h3&gt;
&lt;p&gt;When a customer pays, Stripe sends them back to your site. It is tempting to mark the order paid at that moment. Don't rely on it. The customer can close the tab, lose connection or never reach your success page, and payments can also change state later (failed renewals, refunds, disputes).&lt;/p&gt;
&lt;p&gt;Webhooks are the reliable channel. Your app should treat the webhook as the source of truth for payment state and the redirect only as a friendly &quot;thanks&quot; page.&lt;/p&gt;
&lt;h3 id=&quot;the-six-rules-for-a-safe-webhook-endpoint&quot;&gt;The six rules for a safe webhook endpoint&lt;/h3&gt;
&lt;h4 id=&quot;1-verify-the-signature&quot;&gt;1. Verify the signature&lt;/h4&gt;
&lt;p&gt;Anyone can send a request to your public URL. Stripe signs every event with a &lt;code&gt;Stripe-Signature&lt;/code&gt; header, and you must verify it against your endpoint's signing secret before trusting the payload. Stripe's &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.stripe.com/webhooks&quot;&gt;webhook documentation&lt;/a&gt; explains the scheme. Verification needs the raw request body, so read it before any middleware changes it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Stripe\Webhook;
use Stripe\Exception\SignatureVerificationException;

public function handle(Request $request)
{
    $payload = $request-&amp;gt;getContent();           // raw body
    $signature = $request-&amp;gt;header('Stripe-Signature');

    try {
        $event = Webhook::constructEvent(
            $payload,
            $signature,
            config('services.stripe.webhook_secret')
        );
    } catch (\UnexpectedValueException | SignatureVerificationException $e) {
        return response('Invalid', 400);
    }

    // ...store and queue the event
    return response('OK', 200);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the signing secret in configuration, never in code, and use a separate secret per environment.&lt;/p&gt;
&lt;h4 id=&quot;2-exempt-the-route-from-csrf-and-only-that-route&quot;&gt;2. Exempt the route from CSRF, and only that route&lt;/h4&gt;
&lt;p&gt;Stripe is not a browser, so it cannot send a CSRF token. Exclude the webhook route from CSRF protection (in Laravel 11 and later this is done in &lt;code&gt;bootstrap/app.php&lt;/code&gt; with &lt;code&gt;validateCsrfTokens(except: [...])&lt;/code&gt;). Signature verification replaces CSRF protection for this one endpoint.&lt;/p&gt;
&lt;h4 id=&quot;3-respond-fast-process-later&quot;&gt;3. Respond fast, process later&lt;/h4&gt;
&lt;p&gt;Stripe expects a quick success response. If your handler does slow work, such as sending email, calling other APIs or updating several tables, the request may time out and Stripe will treat it as failed and retry. Do the minimum inside the request: verify, store, queue, return &lt;code&gt;200&lt;/code&gt;. Do the real work in a queued job, as described in my guide to &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-queue-architecture-saas&quot;&gt;Laravel queue architecture&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;4-deduplicate-by-event-id&quot;&gt;4. Deduplicate by event ID&lt;/h4&gt;
&lt;p&gt;Webhooks are delivered at least once, which means duplicates will happen. Every Stripe event has a unique &lt;code&gt;id&lt;/code&gt;. Store it in a table with a unique index. If an insert fails because the ID already exists, you have already seen the event, so return &lt;code&gt;200&lt;/code&gt; and do nothing. The same thinking applies to your own API endpoints, covered in &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-api-idempotency-keys&quot;&gt;API idempotency keys in Laravel&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id=&quot;5-don-t-assume-order&quot;&gt;5. Don't assume order&lt;/h4&gt;
&lt;p&gt;Events can arrive out of order. A &quot;payment failed&quot; event might land after a later &quot;payment succeeded&quot; event for the same invoice. Your handler should look at the current state in your database, and where the order matters, fetch the latest object from Stripe's API instead of trusting the sequence of events.&lt;/p&gt;
&lt;h4 id=&quot;6-make-handlers-safe-to-run-twice&quot;&gt;6. Make handlers safe to run twice&lt;/h4&gt;
&lt;p&gt;Even with deduplication, jobs can be retried. Write each handler so that running it twice leaves the same result: update by a stable key, use &lt;code&gt;updateOrCreate&lt;/code&gt;, and check state before sending an email or granting access.&lt;/p&gt;
&lt;h3 id=&quot;a-reliable-architecture&quot;&gt;A reliable architecture&lt;/h3&gt;
&lt;p&gt;This is the shape I use:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Endpoint:&lt;/strong&gt; verify the signature, insert the event into a &lt;code&gt;stripe_events&lt;/code&gt; table (ID unique, payload, status), dispatch a job, return &lt;code&gt;200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job:&lt;/strong&gt; load the stored event, run the handler for its type, and mark it processed. On failure, let the queue retry with backoff.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handlers:&lt;/strong&gt; one small class per event type you care about, such as &lt;code&gt;invoice.paid&lt;/code&gt;, &lt;code&gt;invoice.payment_failed&lt;/code&gt;, &lt;code&gt;charge.refunded&lt;/code&gt; and &lt;code&gt;checkout.session.completed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dead-letter view:&lt;/strong&gt; a simple admin list of events that failed repeatedly, so a human can inspect and replay them.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Storing the raw event gives you an audit trail, and it lets you replay an event after fixing a bug instead of asking Stripe to resend it.&lt;/p&gt;
&lt;h3 id=&quot;which-events-to-handle&quot;&gt;Which events to handle&lt;/h3&gt;
&lt;p&gt;Don't subscribe to everything. Start with the events that change what the customer can do or what you owe:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A checkout or payment completing successfully&lt;/li&gt;
&lt;li&gt;A payment failing, so you can notify the customer and limit access if appropriate&lt;/li&gt;
&lt;li&gt;Subscription updates and cancellations&lt;/li&gt;
&lt;li&gt;Refunds and disputes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fewer subscriptions mean less code to get wrong. Add more only when a feature needs them.&lt;/p&gt;
&lt;h3 id=&quot;when-stripe-can-t-reach-you&quot;&gt;When Stripe can't reach you&lt;/h3&gt;
&lt;p&gt;If your endpoint returns an error or times out, Stripe retries the delivery over a period of time, with increasing gaps between attempts. You can also resend events from the Stripe dashboard. That safety net only helps if your endpoint is idempotent and your stored events show what happened. Add monitoring so you hear about a failing endpoint before customers do. My notes on &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/laravel-observability-architecture&quot;&gt;observability for Laravel SaaS&lt;/a&gt; cover the alerts I set up.&lt;/p&gt;
&lt;h3 id=&quot;testing-webhooks-locally&quot;&gt;Testing webhooks locally&lt;/h3&gt;
&lt;p&gt;Use the Stripe CLI to forward real test events to your local app and to trigger specific events on demand. Test these cases on purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A valid event, an event with a bad signature and an event with a tampered body&lt;/li&gt;
&lt;li&gt;The same event delivered twice&lt;/li&gt;
&lt;li&gt;Events arriving out of order&lt;/li&gt;
&lt;li&gt;A handler that throws an error halfway through&lt;/li&gt;
&lt;li&gt;Your queue worker being stopped while events arrive&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If all of these behave well in test mode, production will rarely surprise you.&lt;/p&gt;
&lt;h3 id=&quot;should-you-use-laravel-cashier-&quot;&gt;Should you use Laravel Cashier?&lt;/h3&gt;
&lt;p&gt;If you are building standard subscriptions, &lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/billing#handling-stripe-webhooks&quot;&gt;Laravel Cashier&lt;/a&gt; already ships a webhook controller that verifies signatures and handles common subscription events. It is a good starting point, and you can extend it for your own events. For more custom flows, such as saving a card now and charging later, a hand-built handler gives you more control. My article on &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/subscription-billing-architecture-laravel-saas&quot;&gt;subscription billing architecture&lt;/a&gt; covers when each fits.&lt;/p&gt;
&lt;h3 id=&quot;a-real-world-example&quot;&gt;A real-world example&lt;/h3&gt;
&lt;p&gt;On the Stripe lead-billing project, I built a payment workflow that lets lead-generation clients save a payment method without an immediate charge, then automatically charges it when qualified leads are delivered. The status of successful and failed payments was kept in sync through webhook-driven updates. That is exactly the situation where trusting the webhook, not the browser, matters. Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/work/stripe-lead-billing-gohighlevel&quot;&gt;Stripe lead billing case study&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;common-mistakes-to-avoid&quot;&gt;Common mistakes to avoid&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Parsing the JSON body before verifying the signature&lt;/li&gt;
&lt;li&gt;Doing heavy work inside the webhook request&lt;/li&gt;
&lt;li&gt;Treating the checkout redirect as proof of payment&lt;/li&gt;
&lt;li&gt;Using one signing secret for test and live modes&lt;/li&gt;
&lt;li&gt;Returning &lt;code&gt;500&lt;/code&gt; for events you simply don't handle (return &lt;code&gt;200&lt;/code&gt; and ignore them)&lt;/li&gt;
&lt;li&gt;Having no way to see or replay failed events&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Webhooks, not redirects, are the source of truth for payment state.&lt;/li&gt;
&lt;li&gt;Verify the signature, respond fast, then process in a queue.&lt;/li&gt;
&lt;li&gt;Deduplicate on the event ID and make every handler safe to run twice.&lt;/li&gt;
&lt;li&gt;Store raw events so you can audit and replay.&lt;/li&gt;
&lt;li&gt;Monitor the endpoint and test failure cases before launch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want a payment integration that stays correct when things go wrong, &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/book&quot;&gt;book a call&lt;/a&gt; and we can review your flow together.&lt;/p&gt;
&lt;p&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://aqibjavaid.site/blog/stripe-webhooks-laravel-verify-dedupe-retry&quot;&gt;aqibjavaid.site&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31895/stripe-webhooks-in-laravel-verify-dedupe-and-retry-safely</guid>
<pubDate>Wed, 21 Oct 2026 00:02:45 +0000</pubDate>
</item>
<item>
<title>I built a generic multi-step flow engine on top of Laravel controllers</title>
<link>https://coderlegion.com/30439/i-built-a-generic-multi-step-flow-engine-on-top-of-laravel-controllers</link>
<description>&lt;p&gt;If your mobile app decides which step comes next, your backend is trusting the client.&lt;/p&gt;
&lt;p&gt;That's where we were at &lt;a rel=&quot;nofollow&quot; href=&quot;https://getzeney.app&quot;&gt;Zeney Pay&lt;/a&gt;. Sign-in, KYC and transfers were multi-step flows, and half of each flow's logic lived in the app. The backend couldn't tell whether a request to &quot;check the PIN&quot; came after 2FA had actually passed, unless someone remembered to write that check.&lt;/p&gt;
&lt;p&gt;So we moved the whole state machine to the server:&lt;br&gt;
→ every response tells the app which steps are legal next&lt;br&gt;
→ skipping a step gets a 409, and an expired flow gets a 410&lt;br&gt;
→ going back rewinds the flow and discards what came after&lt;br&gt;
→ the app just renders whatever the backend advertises&lt;/p&gt;
&lt;p&gt;I wrote up &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/na2axl/i-built-a-generic-multi-step-flow-engine-on-top-of-laravel-controllers-5f9k&quot;&gt;the original story on dev.to&lt;/a&gt;: the design decisions, how we handled authentication, and the trade-offs we accepted:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/na2axl/i-built-a-generic-multi-step-flow-engine-on-top-of-laravel-controllers-5f9k&quot;&gt;https://dev.to/na2axl/i-built-a-generic-multi-step-flow-engine-on-top-of-laravel-controllers-5f9k&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'll be happy to ear what you think!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30439/i-built-a-generic-multi-step-flow-engine-on-top-of-laravel-controllers</guid>
<pubDate>Fri, 09 Oct 2026 11:33:27 +0000</pubDate>
</item>
<item>
<title>How Much Can You Trust a Filesystem Baseline? Design Notes from Laravel Scalpel</title>
<link>https://coderlegion.com/30211/how-much-can-you-trust-a-filesystem-baseline-design-notes-from-laravel-scalpel</link>
<description>&lt;p&gt;A file hash only means something if you trust the moment it was recorded. That sounds obvious, but it is the part of integrity checking that people most often skip. Here are the design decisions behind the baseline feature in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hryagstn/laravel-scalpel&quot;&gt;Laravel Scalpel&lt;/a&gt;, an open-source intrusion evidence scanner for Laravel. Scalpel is now at v1.11.0, and the trade-offs below came up repeatedly in issues and in an independent review by Laravel News.&lt;/p&gt;
&lt;h3 id=&quot;what-a-baseline-records&quot;&gt;What a baseline records&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;scalpel:baseline&lt;/code&gt; stores three things for each included file: its SHA-256 hash, its size, and its modification time. Later, &lt;code&gt;scalpel:diff&lt;/code&gt; compares the live files against that record and reports files that were added, modified, or deleted.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan scalpel:baseline
php artisan scalpel:diff
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea is simple. Content scanners tell you whether a file looks suspicious. A baseline tells you whether a file &lt;em&gt;changed&lt;/em&gt;, even if it looks perfectly normal. A backdoor hidden in a legitimate file will not match any pattern, but it will not match the old hash either.&lt;/p&gt;
&lt;h3 id=&quot;decision-1-what-to-leave-out&quot;&gt;Decision 1: what to leave out&lt;/h3&gt;
&lt;p&gt;A baseline that reports every log rotation is a baseline you will stop reading. By default, frequently changing paths such as logs, sessions, compiled views, and &lt;code&gt;storage/app&lt;/code&gt; are excluded.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vendor/&lt;/code&gt; gets different treatment. It is skipped by the content scanners, because scanning thousands of third-party files produces noise. But it stays in the baseline comparison. If someone drops a file into an installed package, you still see it in the diff.&lt;/p&gt;
&lt;h3 id=&quot;decision-2-strict-or-fast-hashing&quot;&gt;Decision 2: strict or fast hashing&lt;/h3&gt;
&lt;p&gt;By default, Scalpel runs in strict mode and re-hashes every file on each comparison. That is the most reliable option and also the slowest.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--fast&lt;/code&gt; option first compares size and modification time, and reuses the old hash when both match. It saves work, but it has a known blind spot: an attacker who edits a file and restores its original size and modification time can slip past the baseline comparison. Other scanners may still catch the file by its content or location, but the baseline alone will not.&lt;/p&gt;
&lt;p&gt;A practical rule: use strict mode where the result matters, such as scheduled integrity checks, and &lt;code&gt;--fast&lt;/code&gt; only where speed matters more, such as frequent local runs.&lt;/p&gt;
&lt;h3 id=&quot;decision-3-sign-the-baseline&quot;&gt;Decision 3: sign the baseline&lt;/h3&gt;
&lt;p&gt;If an attacker can rewrite the baseline, the diff becomes meaningless. Scalpel can HMAC-sign baselines and JSON reports when &lt;code&gt;SCALPEL_SIGNING_ENABLED&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. The key goes in &lt;code&gt;SCALPEL_SIGNING_KEY&lt;/code&gt;, and it should be a dedicated key, not your &lt;code&gt;APP_KEY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When signing is on, &lt;code&gt;scalpel:diff&lt;/code&gt; verifies the baseline before using it. A missing or invalid signature is reported as &lt;code&gt;CRITICAL&lt;/code&gt;. For saved JSON reports, &lt;code&gt;scalpel:verify&lt;/code&gt; checks the signature.&lt;/p&gt;
&lt;p&gt;Two details matter in practice. First, turn on signing before you create the first baseline, otherwise you have an unsigned record to start from. Second, signing has a clear limit: it detects a baseline regenerated without the key, but it cannot help if the attacker can read the key or modify the scanner itself.&lt;/p&gt;
&lt;h3 id=&quot;decision-4-when-to-refresh&quot;&gt;Decision 4: when to refresh&lt;/h3&gt;
&lt;p&gt;A baseline is a record of a state you trust, so it has to be refreshed deliberately. The suggested flow after each deployment is to run &lt;code&gt;php artisan optimize&lt;/code&gt;, then &lt;code&gt;scalpel:baseline --force&lt;/code&gt; to replace the old record. Until a baseline exists, both &lt;code&gt;scan&lt;/code&gt; and &lt;code&gt;diff&lt;/code&gt; report a &lt;code&gt;MEDIUM&lt;/code&gt; finding telling you to create one.&lt;/p&gt;
&lt;p&gt;Never create a baseline on a server you suspect is already compromised. You would be recording the attacker's files as normal.&lt;/p&gt;
&lt;h3 id=&quot;the-trust-boundary&quot;&gt;The trust boundary&lt;/h3&gt;
&lt;p&gt;All of this runs in the same process, with the same permissions, as your application. Anyone who can change your code can also change the scanner. So the strongest setup moves the trust boundary outside the app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Trigger scans from outside the server, for example from CI or a scheduler on another machine.&lt;/li&gt;
&lt;li&gt;Keep code directories read-only where possible.&lt;/li&gt;
&lt;li&gt;Send reports and baselines to storage the server cannot write to.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A scanner like this detects evidence of an intrusion. It does not block or contain one.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require hryagstn/laravel-scalpel
php artisan vendor:publish --tag=scalpel-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Requires PHP 8.2 or later and supports Laravel 10 to 13.&lt;/p&gt;
&lt;p&gt;If you have a case where the baseline gives noisy or misleading results, please open an issue. I covered what the individual scanners look for in a separate post on DEV: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/harryagustiana/what-a-filesystem-scan-can-and-cannot-tell-you-after-a-laravel-intrusion-1hn5&quot;&gt;https://dev.to/harryagustiana/what-a-filesystem-scan-can-and-cannot-tell-you-after-a-laravel-intrusion-1hn5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/hryagstn/laravel-scalpel&quot;&gt;https://github.com/hryagstn/laravel-scalpel&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30211/how-much-can-you-trust-a-filesystem-baseline-design-notes-from-laravel-scalpel</guid>
<pubDate>Thu, 08 Oct 2026 04:44:43 +0000</pubDate>
</item>
<item>
<title>JSON vs MessagePack in Laravel: A Practical API Benchmark</title>
<link>https://coderlegion.com/28486/json-vs-messagepack-in-laravel-a-practical-api-benchmark</link>
<description>&lt;p&gt;JSON is the right default for most Laravel APIs. It is readable, supported everywhere, easy to debug, and familiar to every client.&lt;/p&gt;
&lt;p&gt;MessagePack is worth considering when payload size, bandwidth, or a controlled client ecosystem matters. It is a compact binary format that keeps a schema-free data model, but it is not automatically faster or smaller for every workload.&lt;/p&gt;
&lt;p&gt;This article compares both formats in a Laravel API and shows how to adopt MessagePack without breaking existing JSON clients.&lt;/p&gt;
&lt;p&gt;The examples use &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/smmehdisharifi/laravel-msgpack&quot;&gt;Laravel Msgpack&lt;/a&gt;, an open-source package that adds opt-in MessagePack content negotiation, request decoding, safety limits, and a repeatable benchmark command.&lt;/p&gt;
&lt;h3 id=&quot;the-short-answer&quot;&gt;The short answer&lt;/h3&gt;
&lt;p&gt;Use JSON when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your clients include browsers, third-party integrations, or unknown consumers.&lt;/li&gt;
&lt;li&gt;Human-readable responses and simple debugging are important.&lt;/li&gt;
&lt;li&gt;Your payloads are already small or compressed effectively.&lt;/li&gt;
&lt;li&gt;You do not control the client decoder and deployment environment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use MessagePack when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You control the clients and can ship a binary decoder.&lt;/li&gt;
&lt;li&gt;Network transfer size is important for mobile, edge, or internal services.&lt;/li&gt;
&lt;li&gt;Your payloads are large enough for serialization and decoding trade-offs to matter.&lt;/li&gt;
&lt;li&gt;You want to keep JSON compatibility while allowing capable clients to opt in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For many Laravel applications, the best answer is not choosing one format globally. Keep JSON as the default and negotiate MessagePack per request.&lt;/p&gt;
&lt;h3 id=&quot;what-are-we-comparing-&quot;&gt;What are we comparing?&lt;/h3&gt;
&lt;p&gt;JSON is a text representation. It is self-describing, easy to inspect, and supported by the web platform.&lt;/p&gt;
&lt;p&gt;MessagePack is a binary representation. It encodes the same basic data model using compact type markers and lengths instead of textual punctuation and names repeated in a text format.&lt;/p&gt;
&lt;p&gt;The comparison should use the same data, the same PHP runtime, and the same operation count. Otherwise, the result is a benchmark of different workloads rather than a comparison of formats.&lt;/p&gt;
&lt;h3 id=&quot;add-messagepack-to-laravel&quot;&gt;Add MessagePack to Laravel&lt;/h3&gt;
&lt;p&gt;Install the package:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require smmehdisharifi/laravel-msgpack
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply the middleware to a route or route group:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Illuminate\Support\Facades\Route;

Route::middleware('msgpack')-&amp;gt;get('/api/profile', function () {
    return [
        'id' =&amp;gt; 1042,
        'name' =&amp;gt; 'Laravel MessagePack',
        'roles' =&amp;gt; ['maintainer', 'developer'],
        'features' =&amp;gt; [
            'negotiation' =&amp;gt; true,
            'fallback' =&amp;gt; 'json',
        ],
    ];
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The route does not need separate JSON and MessagePack controllers. Existing clients continue to receive JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -i http://localhost/api/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A capable client opts into MessagePack with the Accept header. Save the binary response instead of printing it as terminal text:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sS -D - \
  -H 'Accept: application/msgpack' \
  -o profile.msgpack \
  http://localhost/api/profile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The negotiated response includes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Content-Type: application/msgpack
Vary: Accept
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vary: Accept is important because a cache must not serve a MessagePack response to a client that asked for JSON, or the other way around.&lt;/p&gt;
&lt;h3 id=&quot;how-content-negotiation-works&quot;&gt;How content negotiation works&lt;/h3&gt;
&lt;p&gt;The middleware keeps the migration opt-in:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A client that does not request MessagePack receives JSON.&lt;/li&gt;
&lt;li&gt;A client that sends Accept: application/msgpack receives a binary response when the route supports the middleware.&lt;/li&gt;
&lt;li&gt;If both formats are accepted, the higher quality factor wins.&lt;/li&gt;
&lt;li&gt;Wildcards are evaluated by media-type specificity.&lt;/li&gt;
&lt;li&gt;A request that rejects every supported representation receives 406 Not Acceptable.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The same negotiation applies to relevant error responses, including unmatched routes and route exceptions. Incoming requests can use application/msgpack or the legacy application/x-msgpack media type.&lt;/p&gt;
&lt;h3 id=&quot;run-a-repeatable-benchmark&quot;&gt;Run a repeatable benchmark&lt;/h3&gt;
&lt;p&gt;Laravel Msgpack includes an Artisan command so the comparison is easy to reproduce:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan msgpack:benchmark --iterations=1000 --json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command uses a deterministic API-shaped fixture with nested values and Unicode text. It measures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Raw JSON and MessagePack byte size.&lt;/li&gt;
&lt;li&gt;Encode time per iteration.&lt;/li&gt;
&lt;li&gt;Decode time per iteration.&lt;/li&gt;
&lt;li&gt;Optional gzip size and compression timings when ext-zlib is available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The command warms each operation once before timing it. It reports per-iteration timings so runs with different iteration counts remain comparable.&lt;/p&gt;
&lt;h3 id=&quot;results-from-this-package&quot;&gt;Results from this package&lt;/h3&gt;
&lt;p&gt;The following result was produced by the package benchmark on PHP 8.4 with rybakit/msgpack 0.7.2 and 1,000 iterations:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Measurement &lt;/th&gt;&lt;th&gt;JSON &lt;/th&gt;&lt;th&gt;MessagePack &lt;/th&gt;&lt;th&gt;Difference&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Raw payload &lt;/td&gt;&lt;td&gt;591 B &lt;/td&gt;&lt;td&gt;435 B &lt;/td&gt;&lt;td&gt;26.40% smaller&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Encode time &lt;/td&gt;&lt;td&gt;0.002 ms &lt;/td&gt;&lt;td&gt;0.009 ms &lt;/td&gt;&lt;td&gt;JSON faster&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Decode time &lt;/td&gt;&lt;td&gt;0.003 ms &lt;/td&gt;&lt;td&gt;0.008 ms &lt;/td&gt;&lt;td&gt;JSON faster&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Gzip payload &lt;/td&gt;&lt;td&gt;377 B &lt;/td&gt;&lt;td&gt;371 B &lt;/td&gt;&lt;td&gt;1.59% smaller&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Gzip compress time &lt;/td&gt;&lt;td&gt;0.009 ms &lt;/td&gt;&lt;td&gt;0.010 ms &lt;/td&gt;&lt;td&gt;Nearly equal&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Gzip decompress time &lt;/td&gt;&lt;td&gt;0.004 ms &lt;/td&gt;&lt;td&gt;0.003 ms &lt;/td&gt;&lt;td&gt;Nearly equal&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;There are three useful conclusions here.&lt;/p&gt;
&lt;p&gt;First, MessagePack is noticeably smaller before compression for this fixture. That can matter when transfer size is the bottleneck.&lt;/p&gt;
&lt;p&gt;Second, the smallest representation is not automatically the fastest representation. For this small payload, JSON serialization is faster in the measured run.&lt;/p&gt;
&lt;p&gt;Third, compression changes the size comparison. Once gzip is applied, the gap is much smaller because gzip already removes a lot of JSON's repetitive syntax.&lt;/p&gt;
&lt;p&gt;These are indicative measurements, not universal promises. A real decision should use payloads, client runtimes, compression settings, and network conditions that match your application.&lt;/p&gt;
&lt;h3 id=&quot;decode-messagepack-requests&quot;&gt;Decode MessagePack requests&lt;/h3&gt;
&lt;p&gt;MessagePack request bodies are decoded automatically when the middleware is applied:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Illuminate\Http\Request;

Route::middleware('msgpack')-&amp;gt;post('/api/profile', function (Request $request) {
    return [
        'received' =&amp;gt; $request-&amp;gt;msgpack(),
        'name' =&amp;gt; $request-&amp;gt;input('name'),
    ];
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Map payloads are also merged into Laravel's normal request input. The original decoded value remains available through request()-&amp;gt;msgpack(), including scalar and list payloads.&lt;/p&gt;
&lt;p&gt;The package rejects malformed payloads before they reach the route handler. Configurable request size, nesting-depth, and value-count guards provide additional protection against oversized or unexpectedly deep input.&lt;/p&gt;
&lt;h3 id=&quot;when-a-route-must-always-return-messagepack&quot;&gt;When a route must always return MessagePack&lt;/h3&gt;
&lt;p&gt;Negotiation is useful when the same endpoint serves different clients. For a route that should always return MessagePack, use the response macro:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;return response()-&amp;gt;msgpack([
    'message' =&amp;gt; 'Created',
], 201, [
    'X-Request-Id' =&amp;gt; $requestId,
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Direct serialization is also available through the facade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Msgpack;

$packed = Msgpack::encode($data);
$unpacked = Msgpack::decode($packed);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;a-practical-decision-guide&quot;&gt;A practical decision guide&lt;/h3&gt;
&lt;p&gt;Do not change every endpoint to MessagePack because a synthetic benchmark produced a smaller payload. Instead, measure the part of the system that matters.&lt;/p&gt;
&lt;p&gt;Choose JSON first when interoperability, browser support, observability, or simple debugging is the priority.&lt;/p&gt;
&lt;p&gt;Test MessagePack when bandwidth, payload volume, or mobile transfer cost is important and the clients are controlled.&lt;/p&gt;
&lt;p&gt;Keep both formats when you serve a mixed ecosystem. JSON remains the safe default, while clients that understand MessagePack can opt into it without a second controller implementation.&lt;/p&gt;
&lt;h3 id=&quot;limitations-to-keep-in-mind&quot;&gt;Limitations to keep in mind&lt;/h3&gt;
&lt;p&gt;MessagePack is not encryption. Sensitive data still needs transport security and application-level authorization.&lt;/p&gt;
&lt;p&gt;Binary responses are less convenient to inspect in logs and command-line terminals. Save them to a file or decode them with a client library.&lt;/p&gt;
&lt;p&gt;Compression may reduce the practical size advantage. Measure compressed and uncompressed responses for the transport configuration you actually deploy.&lt;/p&gt;
&lt;p&gt;The benchmark fixture is intentionally stable, not universal. A list-heavy payload, a text-heavy payload, a large nested object, or a response with repeated keys can produce different results.&lt;/p&gt;
&lt;h3 id=&quot;try-it-in-your-project&quot;&gt;Try it in your project&lt;/h3&gt;
&lt;p&gt;Install &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/smmehdisharifi/laravel-msgpack&quot;&gt;Laravel Msgpack&lt;/a&gt;, apply the msgpack middleware to one endpoint, and compare both representations with your real data.&lt;/p&gt;
&lt;p&gt;The repository contains the full implementation, compatibility matrix, configuration reference, safety limits, and benchmark command:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/smmehdisharifi/laravel-msgpack&quot;&gt;https://github.com/smmehdisharifi/laravel-msgpack&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If the package helps your API, &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/smmehdisharifi/laravel-msgpack/stargazers&quot;&gt;a GitHub star&lt;/a&gt; helps other Laravel developers discover it.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/28486/json-vs-messagepack-in-laravel-a-practical-api-benchmark</guid>
<pubDate>Thu, 24 Sep 2026 13:39:50 +0000</pubDate>
</item>
<item>
<title>Building explainable duplicate detection in Laravel (no ML, no packages)</title>
<link>https://coderlegion.com/27941/building-explainable-duplicate-detection-in-laravel-no-ml-no-packages</link>
<description>&lt;p&gt;I built a rental listing platform with &lt;strong&gt;Laravel 13, PHP 8.4, PostgreSQL, Redis, and Sanctum&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;One problem quickly became obvious:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Agents could post the same property multiple times.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In Nigeria, property listings often don't have precise street addresses. A typical listing might look like:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;2-bedroom flat, Yaba. Near Yaba Tech.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There may be no house number and sometimes no street name.&lt;/p&gt;
&lt;p&gt;Instead, people identify properties using a combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Area&lt;/li&gt;
&lt;li&gt;Landmark&lt;/li&gt;
&lt;li&gt;Property type&lt;/li&gt;
&lt;li&gt;Number of bedrooms&lt;/li&gt;
&lt;li&gt;Rent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That makes duplicate detection a little different from what you might build for a marketplace with structured addresses.&lt;/p&gt;
&lt;p&gt;I wanted a solution that was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explainable&lt;/li&gt;
&lt;li&gt;Easy to tune&lt;/li&gt;
&lt;li&gt;Easy for reviewers to understand&lt;/li&gt;
&lt;li&gt;Independent of machine learning&lt;/li&gt;
&lt;li&gt;Cheap enough to run whenever a listing changes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built a &lt;strong&gt;rule-based duplicate detection system using Laravel and PHP&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h3&gt;
&lt;p&gt;The basic workflow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Property created/updated
        ↓
Find candidate properties
        ↓
Calculate similarity score
        ↓
Store score + explanation
        ↓
Classify match
        ↓
Reviewer confirms/rejects
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important distinction is that I don't compare every property against every other property.&lt;/p&gt;
&lt;p&gt;Instead, I first narrow the candidate set using database queries and then perform the more expensive comparison in PHP.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;1-creating-a-property-fingerprint&quot;&gt;1. Creating a Property Fingerprint&lt;/h2&gt;
&lt;p&gt;Because Nigerian rental listings don't always have reliable addresses, I needed another way to represent a property's identity.&lt;/p&gt;
&lt;p&gt;I started with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Area + Landmark + Bedrooms + Property Type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These fields are normalized and hashed into a fingerprint.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class PropertyFingerprintService
{
    public static function generate(array $data): string
    {
        $parts = [
            strtolower(trim($data['area'] ?? '')),
            strtolower(trim($data['landmark'] ?? '')),
            (string) ($data['bedrooms'] ?? ''),
            strtolower(trim($data['property_type'] ?? '')),
        ];

        return hash('sha256', implode('|', $parts));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Yaba | Near Yaba Tech | 2 | Flat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;becomes a deterministic SHA-256 hash.&lt;/p&gt;
&lt;p&gt;This gives us a fast way to identify listings that have the same core identity fields.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;an-important-database-lesson&quot;&gt;An Important Database Lesson&lt;/h3&gt;
&lt;p&gt;Initially, I made the fingerprint column unique:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$table-&amp;gt;string('fingerprint')-&amp;gt;unique();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That turned out to be a mistake.&lt;/p&gt;
&lt;p&gt;If an agent tried to create the same property again, the database rejected it.&lt;/p&gt;
&lt;p&gt;That sounds useful until you remember the purpose of the feature.&lt;/p&gt;
&lt;p&gt;I don't want the database to prevent the duplicate.&lt;/p&gt;
&lt;p&gt;I want the duplicate to exist so that the platform can &lt;strong&gt;surface it for human review&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So I changed the migration to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$table-&amp;gt;dropUnique(['fingerprint']);
$table-&amp;gt;index('fingerprint');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now multiple properties can have the same fingerprint.&lt;/p&gt;
&lt;p&gt;The database helps us &lt;strong&gt;find duplicates instead of blocking them&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That distinction is important for any system where duplicates are supposed to be reviewed rather than automatically rejected.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-candidate-selection&quot;&gt;2. Candidate Selection&lt;/h2&gt;
&lt;p&gt;The next problem is scale.&lt;/p&gt;
&lt;p&gt;Imagine the platform has 100,000 properties.&lt;/p&gt;
&lt;p&gt;Comparing one new property against all 100,000 would obviously be wasteful.&lt;/p&gt;
&lt;p&gt;Instead, I narrow the candidates using fields that are already indexed.&lt;/p&gt;
&lt;p&gt;The query looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function findCandidates(Property $property): EloquentCollection
{
    return Property::query()
        -&amp;gt;where('id', '!=', $property-&amp;gt;id)
        -&amp;gt;where(function ($query) use ($property) {
            $query-&amp;gt;where('fingerprint', $property-&amp;gt;fingerprint)
                -&amp;gt;orWhere(function ($inner) use ($property) {
                    $inner-&amp;gt;where('area', $property-&amp;gt;area)
                        -&amp;gt;where('bedrooms', $property-&amp;gt;bedrooms);
                });

            if ($property-&amp;gt;landmark) {
                $query-&amp;gt;orWhere('landmark', $property-&amp;gt;landmark);
            }
        })
        -&amp;gt;get();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query looks simple, but there is an important Laravel detail here.&lt;/p&gt;
&lt;h3 id=&quot;group-your-orwhere-clauses&quot;&gt;Group your &lt;code&gt;orWhere&lt;/code&gt; clauses&lt;/h3&gt;
&lt;p&gt;The exclusion:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;-&amp;gt;where('id', '!=', $property-&amp;gt;id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;needs to apply to every branch of the candidate query.&lt;/p&gt;
&lt;p&gt;That's why the &lt;code&gt;orWhere&lt;/code&gt; conditions are wrapped in a closure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;-&amp;gt;where(function ($query) {
    // OR conditions
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without that grouping, you can accidentally generate logic equivalent to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;id != ?
AND fingerprint = ?
OR area = ?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;OR&lt;/code&gt; can escape the intended grouping and return the current property itself.&lt;/p&gt;
&lt;p&gt;For queries with multiple &lt;code&gt;OR&lt;/code&gt; conditions, explicit grouping is worth paying attention to.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-scoring-the-candidates&quot;&gt;3. Scoring the Candidates&lt;/h2&gt;
&lt;p&gt;Once I have a relatively small candidate set, I score each property against the new listing.&lt;/p&gt;
&lt;p&gt;The scoring model is intentionally simple.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Signal &lt;/th&gt;&lt;th&gt;Points&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Same area &lt;/td&gt;&lt;td&gt;25&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Same landmark &lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Same bedrooms &lt;/td&gt;&lt;td&gt;15&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Rent within 10% &lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Description similarity &lt;/td&gt;&lt;td&gt;20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Maximum&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;100&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The goal isn't to produce a statistically perfect probability.&lt;/p&gt;
&lt;p&gt;The goal is to produce a number that a reviewer can understand.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;keeping-the-explanation&quot;&gt;Keeping the Explanation&lt;/h3&gt;
&lt;p&gt;Before calculating the score, I create a details structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$details = [
    'area_match' =&amp;gt; false,
    'landmark_match' =&amp;gt; false,
    'bedrooms_match' =&amp;gt; false,
    'rent_match' =&amp;gt; null,
    'rent_difference_percent' =&amp;gt; null,
    'description_similarity' =&amp;gt; 0.0,
    'fingerprint_match' =&amp;gt; false,
];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is important because storing only:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;confidence_score = 87
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;doesn't tell us much.&lt;/p&gt;
&lt;p&gt;Instead, we can store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;area_match&quot;: true,
    &quot;landmark_match&quot;: true,
    &quot;bedrooms_match&quot;: true,
    &quot;rent_match&quot;: &quot;within_10_percent&quot;,
    &quot;rent_difference_percent&quot;: 4.2,
    &quot;description_similarity&quot;: 87.4,
    &quot;fingerprint_match&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the reviewer can understand why the system flagged the properties.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-comparing-the-area&quot;&gt;4. Comparing the Area&lt;/h2&gt;
&lt;p&gt;The area comparison is straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;if (strcasecmp(trim($a-&amp;gt;area), trim($b-&amp;gt;area)) === 0) {
    $details['area_match'] = true;

    $points += self::WEIGHT_AREA;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;private const WEIGHT_AREA = 25;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-comparing-rent&quot;&gt;5. Comparing Rent&lt;/h2&gt;
&lt;p&gt;Rent shouldn't need to be exactly the same.&lt;/p&gt;
&lt;p&gt;Different agents might advertise the same property at slightly different prices.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Agent A: ₦1,500,000
Agent B: ₦1,550,000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These listings could still represent the same property.&lt;/p&gt;
&lt;p&gt;So I calculate the percentage difference:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;if ((float) $a-&amp;gt;rent &amp;gt; 0 &amp;amp;&amp;amp; (float) $b-&amp;gt;rent &amp;gt; 0) {
    $diff = abs((float) $a-&amp;gt;rent - (float) $b-&amp;gt;rent)
        / max((float) $a-&amp;gt;rent, (float) $b-&amp;gt;rent);

    $details['rent_difference_percent'] = round($diff * 100, 2);

    if ($diff &amp;lt;= 0.10) {
        $details['rent_match'] = 'within_10_percent';

        $points += self::WEIGHT_RENT;
    } elseif ($diff &amp;lt;= 0.20) {
        $details['rent_match'] = 'within_20_percent';

        $points += self::WEIGHT_RENT / 2;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Within 10% → 20 points&lt;/li&gt;
&lt;li&gt;Within 20% → 10 points&lt;/li&gt;
&lt;li&gt;More than 20% → 0 points&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This gives us a little tolerance without treating every price as equivalent.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-comparing-descriptions&quot;&gt;6. Comparing Descriptions&lt;/h2&gt;
&lt;p&gt;For descriptions, I used PHP's built-in &lt;code&gt;similar_text()&lt;/code&gt; rather than introducing another package.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;similar_text(
    strtolower($a-&amp;gt;description),
    strtolower($b-&amp;gt;description),
    $sim
);

if ($sim &amp;gt;= 80) {
    $points += self::WEIGHT_DESCRIPTION;
} elseif ($sim &amp;gt;= 60) {
    $points += self::WEIGHT_DESCRIPTION / 2;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The thresholds are:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;80%+ → 20 points
60–79% → 10 points
&amp;lt;60% → 0 points
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works reasonably well for short rental descriptions.&lt;/p&gt;
&lt;p&gt;It is not intended to be a sophisticated natural-language similarity model.&lt;/p&gt;
&lt;p&gt;And that's okay for this stage.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-the-fingerprint-rule&quot;&gt;7. The Fingerprint Rule&lt;/h2&gt;
&lt;p&gt;A matching fingerprint is a strong signal:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Area
+
Landmark
+
Bedrooms
+
Property type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If those fields all match, I want the listing to be reviewed even if some other signals differ.&lt;/p&gt;
&lt;p&gt;The final score is calculated like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$confidenceScore = round(min($points, 100), 2);

if ($details['fingerprint_match']) {
    $confidenceScore = max($confidenceScore, 90.0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So a fingerprint match effectively creates a minimum score of 90.&lt;/p&gt;
&lt;p&gt;That means the system classifies it as a very likely duplicate.&lt;/p&gt;
&lt;p&gt;This is a deliberate product decision rather than a mathematical requirement.&lt;/p&gt;
&lt;p&gt;Later, this should probably become configurable because two properties can theoretically share the same area, landmark, bedrooms and type while still being different properties.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-turning-the-score-into-a-match-level&quot;&gt;8. Turning the Score Into a Match Level&lt;/h2&gt;
&lt;p&gt;I use an enum to map the score into a meaningful state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public static function fromScore(float $score): self
{
    return match (true) {
        $score &amp;gt;= 90 =&amp;gt; self::VERY_LIKELY,
        $score &amp;gt;= 70 =&amp;gt; self::POSSIBLE,
        default      =&amp;gt; self::UNIQUE,
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting categories are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Score &lt;/th&gt;&lt;th&gt;Level&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;90–100 &lt;/td&gt;&lt;td&gt;Very likely&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;70–89 &lt;/td&gt;&lt;td&gt;Possible&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&amp;lt;70 &lt;/td&gt;&lt;td&gt;Unique&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The UI doesn't need to know how the score was calculated.&lt;/p&gt;
&lt;p&gt;It simply displays the appropriate match level and the underlying explanation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-persisting-the-match-details&quot;&gt;9. Persisting the Match Details&lt;/h2&gt;
&lt;p&gt;The score and explanation are stored on a &lt;code&gt;duplicate_matches&lt;/code&gt; record.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;property_id
matched_property_id
confidence_score
match_details
status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;match_details&lt;/code&gt; column is JSON.&lt;/p&gt;
&lt;p&gt;That means I can store the complete scoring breakdown without creating a separate database column for every signal.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &quot;area_match&quot;: true,
    &quot;landmark_match&quot;: true,
    &quot;bedrooms_match&quot;: true,
    &quot;rent_match&quot;: &quot;within_10_percent&quot;,
    &quot;rent_difference_percent&quot;: 4.2,
    &quot;description_similarity&quot;: 87.4,
    &quot;fingerprint_match&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reviewer UI can then display something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Very likely duplicate — 94

✓ Same area
✓ Same landmark
✓ Same bedrooms
✓ Rent within 10%
✓ Similar description
✓ Matching property fingerprint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's much more useful than simply saying:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Duplicate confidence: 94%&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-reviewer-decisions-must-survive-edits&quot;&gt;10. Reviewer Decisions Must Survive Edits&lt;/h2&gt;
&lt;p&gt;There is another problem with running detection every time a property changes.&lt;/p&gt;
&lt;p&gt;Suppose a reviewer sees:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Property A ↔ Property B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and decides:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;These are different properties.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The reviewer rejects the match.&lt;/p&gt;
&lt;p&gt;Then the agent edits the listing.&lt;/p&gt;
&lt;p&gt;If &lt;code&gt;detect()&lt;/code&gt; simply runs again and recreates the match, the reviewer gets the same warning again.&lt;/p&gt;
&lt;p&gt;That's frustrating.&lt;/p&gt;
&lt;p&gt;So duplicate matches have state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pending
rejected
confirmed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When checking an existing match:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$existing = DuplicateMatch::query()
    -&amp;gt;where('property_id', $property-&amp;gt;id)
    -&amp;gt;where('matched_property_id', $candidate-&amp;gt;id)
    -&amp;gt;first();

if ($existing &amp;amp;&amp;amp; $existing-&amp;gt;status === 'rejected' &amp;amp;&amp;amp; ! $rescan) {
    continue;
}

if ($existing &amp;amp;&amp;amp; $existing-&amp;gt;status === 'confirmed') {
    continue;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A rejected match stays rejected during normal edits.&lt;/p&gt;
&lt;p&gt;A manual rescan can explicitly override that decision.&lt;/p&gt;
&lt;p&gt;This makes the system behave more like a workflow rather than just a calculation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;11-avoiding-timeline-noise&quot;&gt;11. Avoiding Timeline Noise&lt;/h2&gt;
&lt;p&gt;Duplicate detection can run frequently.&lt;/p&gt;
&lt;p&gt;If every score recalculation creates a timeline event, the property history becomes noisy.&lt;/p&gt;
&lt;p&gt;So I only log meaningful changes.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$scoreChanged = $existing
    &amp;amp;&amp;amp; abs(
        (float) $existing-&amp;gt;confidence_score
        - $score-&amp;gt;confidenceScore
    ) &amp;gt;= 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A timeline event is created when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new match is detected&lt;/li&gt;
&lt;li&gt;A manual rescan happens&lt;/li&gt;
&lt;li&gt;The score changes significantly&lt;/li&gt;
&lt;li&gt;A reviewer confirms/rejects a match&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This keeps the audit trail useful.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;12-where-detection-runs&quot;&gt;12. Where Detection Runs&lt;/h2&gt;
&lt;p&gt;The detector runs after the property has been saved.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// PropertyCreator

$this-&amp;gt;duplicateDetection-&amp;gt;detect(
    $property,
    $user
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And after an update:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// PropertyController@update

$this-&amp;gt;duplicateDetection-&amp;gt;detect(
    $property-&amp;gt;fresh(),
    $user
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is also useful for bulk imports.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;.xlsx&lt;/code&gt; importer reuses the same property creation workflow.&lt;/p&gt;
&lt;p&gt;That means imported properties automatically go through duplicate detection without implementing another version of the algorithm.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;13-testing-the-detector&quot;&gt;13. Testing the Detector&lt;/h2&gt;
&lt;p&gt;I didn't just test properties that I expected to be duplicates.&lt;/p&gt;
&lt;p&gt;I created test spreadsheets containing both duplicates and controls.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Scenario &lt;/th&gt;&lt;th&gt;Score&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Same fingerprint + cosmetic differences &lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Same area + different landmark + similar rent/description &lt;/td&gt;&lt;td&gt;80&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Duplicate properties in one import &lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Same area but different size &lt;/td&gt;&lt;td&gt;25&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Same area + bedrooms but different rent/description &lt;/td&gt;&lt;td&gt;40&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The control cases are extremely important.&lt;/p&gt;
&lt;p&gt;Without them, it's easy to keep increasing the number of signals until almost every property gets flagged.&lt;/p&gt;
&lt;p&gt;A duplicate detector isn't useful if reviewers don't trust its results.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;14-what-i-d-fix-next&quot;&gt;14. What I'd Fix Next&lt;/h2&gt;
&lt;p&gt;The current implementation works, but there are several areas I would improve.&lt;/p&gt;
&lt;h3 id=&quot;canonical-areas-and-landmarks&quot;&gt;Canonical Areas and Landmarks&lt;/h3&gt;
&lt;p&gt;Exact string matching creates problems:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Yaba
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Yaba, Lagos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;are treated as different.&lt;/p&gt;
&lt;p&gt;Similarly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Opp. Yaba Tech
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Opposite Yaba Tech
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;are different strings.&lt;/p&gt;
&lt;p&gt;A better approach would be a canonical area/landmark system with aliases.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Yaba
├── Yaba, Lagos
├── Yaba Lagos
└── Yaba Mainland
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;could all resolve to the same canonical identifier.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;case-sensitive-candidate-queries&quot;&gt;Case-Sensitive Candidate Queries&lt;/h3&gt;
&lt;p&gt;The scoring logic uses:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;strcasecmp()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but the candidate query uses normal SQL equality.&lt;/p&gt;
&lt;p&gt;That creates an inconsistency.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Yaba
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;yaba
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;may score as a match but never become candidates in the first place.&lt;/p&gt;
&lt;p&gt;With PostgreSQL, normalized columns or &lt;code&gt;LOWER()&lt;/code&gt; expression indexes would make this more consistent.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bedroom-type-casting&quot;&gt;Bedroom Type Casting&lt;/h3&gt;
&lt;p&gt;The database may return:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while another value might be:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&quot;2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If strict comparisons are used, those can behave differently.&lt;/p&gt;
&lt;p&gt;Casting the model attribute is safer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;protected $casts = [
    'bedrooms' =&amp;gt; 'integer',
];
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;normalizing-match-pairs&quot;&gt;Normalizing Match Pairs&lt;/h3&gt;
&lt;p&gt;Currently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;B → A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can become separate records.&lt;/p&gt;
&lt;p&gt;That means reviewers could potentially see the same duplicate pair twice.&lt;/p&gt;
&lt;p&gt;A better approach is to normalize the IDs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$firstId = min($property-&amp;gt;id, $candidate-&amp;gt;id);
$secondId = max($property-&amp;gt;id, $candidate-&amp;gt;id);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then always store:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;first_id
second_id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in a consistent order.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;similar-text-performance&quot;&gt;&lt;code&gt;similar_text()&lt;/code&gt; Performance&lt;/h3&gt;
&lt;p&gt;PHP's &lt;code&gt;similar_text()&lt;/code&gt; is convenient, but it isn't something I'd want to run against large amounts of text indefinitely.&lt;/p&gt;
&lt;p&gt;For short rental descriptions, it's acceptable.&lt;/p&gt;
&lt;p&gt;If descriptions become long or the candidate set grows significantly, I'd add a cheaper prefilter before running the similarity calculation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-fingerprint-floor&quot;&gt;The Fingerprint Floor&lt;/h3&gt;
&lt;p&gt;Currently:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;if ($details['fingerprint_match']) {
    $confidenceScore = max($confidenceScore, 90);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means matching identity fields can result in a duplicate score of 90 even when the rent is very different.&lt;/p&gt;
&lt;p&gt;Sometimes that's correct.&lt;/p&gt;
&lt;p&gt;Sometimes it isn't.&lt;/p&gt;
&lt;p&gt;I'd eventually make the fingerprint floor configurable so product/operations can tune the behavior based on real reviewer decisions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;15-why-i-didn-t-start-with-machine-learning&quot;&gt;15. Why I Didn't Start With Machine Learning&lt;/h2&gt;
&lt;p&gt;There are three main reasons.&lt;/p&gt;
&lt;h4 id=&quot;1-there-wasn-t-enough-training-data&quot;&gt;1. There wasn't enough training data&lt;/h4&gt;
&lt;p&gt;A new marketplace doesn't have thousands of confirmed duplicate/non-duplicate examples.&lt;/p&gt;
&lt;p&gt;Without good labelled data, an ML model isn't automatically going to solve the problem.&lt;/p&gt;
&lt;h4 id=&quot;2-reviewers-need-explanations&quot;&gt;2. Reviewers need explanations&lt;/h4&gt;
&lt;p&gt;If a reviewer asks:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Why did you flag these two properties?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I can answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Same area, same landmark, same bedrooms, rent is 4.2% apart, and the descriptions are 87% similar.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's actionable.&lt;/p&gt;
&lt;p&gt;A black-box prediction isn't as useful for an operational workflow.&lt;/p&gt;
&lt;h4 id=&quot;3-the-weights-are-easy-to-change&quot;&gt;3. The weights are easy to change&lt;/h4&gt;
&lt;p&gt;The scoring weights are centralized:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;private const WEIGHT_AREA = 25;
private const WEIGHT_LANDMARK = 20;
private const WEIGHT_BEDROOMS = 15;
private const WEIGHT_RENT = 20;
private const WEIGHT_DESCRIPTION = 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If reviewer feedback shows that one signal is too aggressive, I can tune it without introducing a new model-training pipeline.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-bigger-lesson&quot;&gt;The Bigger Lesson&lt;/h2&gt;
&lt;p&gt;The interesting part of this project wasn't really the scoring formula.&lt;/p&gt;
&lt;p&gt;It was the workflow around it.&lt;/p&gt;
&lt;p&gt;A useful duplicate detection system needs to answer more than:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Are these two properties similar?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It also needs to answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why were they flagged?&lt;/li&gt;
&lt;li&gt;What happens when a reviewer disagrees?&lt;/li&gt;
&lt;li&gt;Does that decision survive future edits?&lt;/li&gt;
&lt;li&gt;Can the reviewer rescan?&lt;/li&gt;
&lt;li&gt;Can the match be confirmed?&lt;/li&gt;
&lt;li&gt;Can the listings eventually be merged?&lt;/li&gt;
&lt;li&gt;Can we audit what happened?&lt;/li&gt;
&lt;li&gt;Can the scoring rules be changed later?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The detection algorithm is only one part of the feature.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;review workflow is what makes the detection useful in production.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;
&lt;p&gt;If you're building duplicate detection for a marketplace, especially in a market without clean addresses:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't put a unique constraint on something humans are supposed to review.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Narrow candidates in SQL, then perform detailed scoring in application code.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Group your &lt;code&gt;orWhere&lt;/code&gt; clauses carefully.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Store the explanation, not just the score.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat reviewer decisions as persistent state.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test controls, not only the duplicates you want to catch.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with deterministic rules you can explain.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use real reviewer decisions to improve the rules over time.&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You don't always need machine learning to build useful marketplace intelligence.&lt;/p&gt;
&lt;p&gt;Sometimes the best first version is a simple system that understands the signals your market actually has, produces a result people can explain, and gives humans the final say.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27941/building-explainable-duplicate-detection-in-laravel-no-ml-no-packages</guid>
<pubDate>Mon, 21 Sep 2026 11:53:07 +0000</pubDate>
</item>
<item>
<title>How to monitor and maintain Laravel apps for multiple clients</title>
<link>https://coderlegion.com/26305/how-to-monitor-and-maintain-laravel-apps-for-multiple-clients</link>
<description>&lt;p&gt;Forge and Vapor keep the stack alive. Front-end Core Web Vitals need a separate URL list, schedule, and alert path across every client app.&lt;/p&gt;
&lt;p&gt;Laravel agencies often run a clean ops story and a messy front-end story at the same time. Forge or Vapor shows green deployments, queues drain, and uptime probes stay quiet, while PageSpeed Insights on the same client’s login and checkout routes still reports slow Largest Contentful Paint or sticky Interaction to Next Paint. That split is not a tooling failure. It is two different jobs under one “Laravel monitoring” label.&lt;/p&gt;
&lt;p&gt;What follows is a practical playbook for &lt;strong&gt;monitoring and maintaining Laravel apps across multiple clients&lt;/strong&gt; without pretending that application performance monitoring replaces Core Web Vitals coverage. We write from the agency side of that portfolio: shared packages, repeated Livewire patterns, and clients who expect both “the app is up” and “the pages feel fast.” The goal is a stack you can explain in a retainer meeting without collapsing three instruments into one vague promise.&lt;/p&gt;
&lt;h3 id=&quot;what-does-monitoring-laravel-for-multiple-clients-actually-include-&quot;&gt;What does monitoring Laravel for multiple clients actually include?&lt;/h3&gt;
&lt;p&gt;In practice the phrase covers three jobs. Mixing them in one Slack channel is how teams miss regressions for weeks. Naming the jobs in the retainer pack also reduces the chance that sales language oversells whatever tool the team already bought.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Job &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Typical tools &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Question it answers&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Infrastructure and deployments&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Laravel Forge, Vapor, server metrics, SSL expiry &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Is the host healthy and is the release path working?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Application performance (APM)&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Laravel-native APM, exception trackers, queue/job tracing &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Which requests, queries, or jobs are slow or failing in production?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Front-end Core Web Vitals&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;PageSpeed Insights / Lighthouse lab, CrUX where available, scheduled portfolio checks &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Do key &lt;strong&gt;browser&lt;/strong&gt; URLs stay within lab and field budgets after deployments and content changes?&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Forge and Vapor are built for infrastructure and deployments: hosts, release paths, SSL, and basic health. APM products are built for server timing, queries, and failed jobs. Neither automatically watches Largest Contentful Paint on &lt;code&gt;/login&lt;/code&gt;, Cumulative Layout Shift on a marketing landing page that shares the same Blade layout, or Interaction to Next Paint on a Livewire-heavy dashboard. Those are document and interaction metrics measured in the browser, not PHP unit traces.&lt;/p&gt;
&lt;p&gt;Agencies that only watch infrastructure and APM still get surprised when a shared Blade layout ships a heavier hero script and every client’s mobile lab score drops on the same Thursday.&lt;/p&gt;
&lt;h3 id=&quot;how-do-forge-vapor-and-apm-differ-from-front-end-pagespeed-budgets-&quot;&gt;How do Forge, Vapor, and APM differ from front-end PageSpeed budgets?&lt;/h3&gt;
&lt;p&gt;A useful split is by what each tool measures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ops checks&lt;/strong&gt; care about deployment success, worker heartbeat, and HTTP 200 on a health endpoint. &lt;strong&gt;APM checks&lt;/strong&gt; care about server timing, query count, and exception rates. &lt;strong&gt;PageSpeed checks&lt;/strong&gt; care about what Chrome paints and responds to on named URLs, on mobile and desktop, with lab runs you can schedule and field data Google publishes when the URL qualifies for CrUX.&lt;/p&gt;
&lt;p&gt;A Vapor environment can be perfectly healthy while a client’s authenticated dashboard still ships a megabyte of JavaScript on first paint. An APM flame graph can look calm while a cookie banner collapses after load and field Cumulative Layout Shift goes amber. Those are not contradictions. They are different instruments.&lt;/p&gt;
&lt;p&gt;For a portfolio of Laravel clients, reporting stays clearer when those jobs stay separate:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ops / APM: “Is the application reliable?”&lt;/li&gt;
&lt;li&gt;Front-end budgets: “Are the checkout and login URLs still acceptable for users and Search Console?”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the monthly pack only covers reliability, clients often hear “we monitor Laravel” and assume Core Web Vitals are covered. They are not. Both answers in the same retainer pack keep the language aligned with the instruments you actually run.&lt;/p&gt;
&lt;h3 id=&quot;which-urls-belong-on-a-laravel-core-web-vitals-list-&quot;&gt;Which URLs belong on a Laravel Core Web Vitals list?&lt;/h3&gt;
&lt;p&gt;A homepage-only list rarely covers Laravel apps that sell, book, or administer work. A short, stack-agnostic set of route &lt;strong&gt;classes&lt;/strong&gt; tends to survive theme and package churn better; concrete paths can then be filled per client so the schedule still makes sense after a redesign.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Public marketing or landing URL&lt;/strong&gt; that shares the app’s Blade layout (often the sales site or &lt;code&gt;/&lt;/code&gt; on the same domain).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication entry&lt;/strong&gt; (&lt;code&gt;/login&lt;/code&gt;, &lt;code&gt;/register&lt;/code&gt;, SSO start) where fonts, Alpine/Livewire boot, and third-party widgets collide.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Primary authenticated shell&lt;/strong&gt; (dashboard home, “today” view, or first screen after login).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checkout or conversion URL&lt;/strong&gt; (checkout, booking confirm, quote submit, subscription upgrade) even if it sits behind auth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One high-traffic content or catalogue URL&lt;/strong&gt; if the app mixes editorial or product listing with the Laravel stack.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ten carefully chosen URLs usually beat fifty forgotten ones. Mobile and desktop often deserve separate entries when the authenticated shell is a different layout on phone.&lt;/p&gt;
&lt;p&gt;When discovery and scheduling run across many hosts, that short list works well as the seed set to protect after sitemap crawl adds noise. Utility routes and pagination URLs will dilute the budget list if every discovered path is accepted. We walk through that multi-site setup pattern in &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-monitor-maintain-laravel-apps-multiple-clients&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;why-do-shared-laravel-components-regress-every-client-at-once-&quot;&gt;Why do shared Laravel components regress every client at once?&lt;/h3&gt;
&lt;p&gt;Multi-client Laravel work rarely means twenty unrelated codebases. Agencies reuse private packages, a house Blade kit, Livewire components, Spatie packages, Filament resources, or a shared Vite build. That reuse is good for maintenance until a single merge changes paint cost for every client that shares the kit.&lt;/p&gt;
&lt;p&gt;Shared pieces that often move together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layout Blade&lt;/strong&gt; that injects analytics, chat, or A/B scripts on every response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Livewire / Alpine&lt;/strong&gt; widgets that hydrate large tables on the dashboard home.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Font and CSS pipelines&lt;/strong&gt; shared through a design package.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-party tags&lt;/strong&gt; added once in a layout and inherited by every client fork.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Image and media helpers&lt;/strong&gt; that resize differently after a package bump.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the regression is shared, APM may still look fine (PHP time unchanged) while Largest Contentful Paint and Interaction to Next Paint move together on every client that pulled the package. Front-end checks on the same URL classes across clients make one bad package release read as a portfolio event, not five unrelated tickets. That is how the shared-kit advantage stays useful without turning every package bump into a surprise client call.&lt;/p&gt;
&lt;h3 id=&quot;when-is-scheduled-pagespeed-enough-and-when-do-you-still-need-apm-&quot;&gt;When is scheduled PageSpeed enough, and when do you still need APM?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Scheduled PageSpeed (lab) plus field context where CrUX exists&lt;/strong&gt; fits when the question is user-visible speed on known URLs after deployments, content edits, or tag-manager changes. That coverage answers client questions about Search Console Core Web Vitals and “why does the dashboard feel heavy on mobile.” It is also the evidence pack you can put next to a release note without opening Forge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;APM and exception monitoring&lt;/strong&gt; still matter when the question is slow endpoints, failing jobs, N+1 queries, or memory spikes. Those bugs do not always show up as Lighthouse Performance score changes, and Lighthouse will not tell you which queue worker stalled overnight. The tools are complementary rather than competing line items on the invoice.&lt;/p&gt;
&lt;p&gt;A durable agency stack for Laravel portfolios often has three layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Hosting / Forge / Vapor for deployments and basic health.&lt;/li&gt;
&lt;li&gt;APM / errors for application internals.&lt;/li&gt;
&lt;li&gt;Scheduled multi-site PageSpeed for front-end budgets and client-facing evidence.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Skipping layer three is common when the team’s identity is “Laravel shop.” Skipping layer two is common when the team’s identity is “SEO shop.” Clients feel both gaps.&lt;/p&gt;
&lt;h3 id=&quot;where-does-a-multi-tenant-pagespeed-tool-fit-for-laravel-agencies-&quot;&gt;Where does a multi-tenant PageSpeed tool fit for Laravel agencies?&lt;/h3&gt;
&lt;p&gt;If you already open five PageSpeed tabs per client after every release, the gap is rarely another lecture on why manual checks fail. It is usually one place to keep organisations separate, attach the URL classes above, run schedules, and see which client site needs attention this week. That is operational maintenance, not a new ideology about Laravel.&lt;/p&gt;
&lt;p&gt;That is the job of a multi-tenant PageSpeed portfolio tool, not a replacement for Forge or for Laravel APM. We built &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-monitor-maintain-laravel-apps-multiple-clients&quot;&gt;Apogee Watcher&lt;/a&gt; for that front-end layer: scheduled PageSpeed Insights across many client sites, discovery when routes appear, and a dashboard shaped for agency roles. The product spotlight on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-multiple-client-sites-one-dashboard?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-monitor-maintain-laravel-apps-multiple-clients&quot;&gt;managing multiple client sites in one dashboard&lt;/a&gt; shows how that dashboard differs from a flat project list.&lt;/p&gt;
&lt;p&gt;Honest limits matter on Hashnode as much as on our own blog. Watcher does not replace Telescope, Nightwatch-class APM, or Forge server graphs. It holds front-end evidence so Laravel agencies can maintain &lt;strong&gt;many&lt;/strong&gt; apps without pretending ops green equals Core Web Vitals green.&lt;/p&gt;
&lt;h3 id=&quot;faq-1&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Does Laravel Forge monitoring cover Core Web Vitals?&lt;/strong&gt;&lt;br&gt;
No. Forge watches servers, deployments, and related ops signals. Core Web Vitals are measured on browser navigations to specific URLs. Lab and field coverage for those URLs still sits outside Forge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should every Laravel client get the same URL list?&lt;/strong&gt;&lt;br&gt;
The same &lt;strong&gt;classes&lt;/strong&gt; work well across clients (login, authenticated shell, checkout or conversion URL, public landing). Concrete paths still differ per app. A Filament admin and a Jetstream consumer app will not share identical routes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is Lighthouse CI enough for a multi-client book?&lt;/strong&gt;&lt;br&gt;
Lighthouse CI is strong in the pipeline for apps you control end to end. Agency portfolios still benefit from scheduled production checks when not every client sits on the same CI project, and when marketing tags change outside your repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can APM replace PageSpeed Insights for client reporting?&lt;/strong&gt;&lt;br&gt;
Not for Search Console or for “how fast does this page feel in Chrome.” APM explains server work. PageSpeed Insights and CrUX explain what users experience on the document.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a sensible first alert set across Laravel clients?&lt;/strong&gt;&lt;br&gt;
Budget breaches on the authenticated shell and the checkout or conversion URL on mobile usually catch shared layout and script regressions before a homepage-only check does.&lt;/p&gt;
&lt;h3 id=&quot;cta&quot;&gt;CTA&lt;/h3&gt;
&lt;p&gt;If you maintain several Laravel apps for clients, a useful next step is to sketch the three-layer stack on one page: ops, APM, and front-end URL budgets. Seeding the Core Web Vitals list from the classes above, putting it on a schedule, and treating shared package releases as portfolio events usually closes the gap faster than another round of ad-hoc PageSpeed Insights tabs. When you want that front-end layer in one multi-tenant product, &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/sign-up?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-monitor-maintain-laravel-apps-multiple-clients&quot;&gt;try Apogee Watcher&lt;/a&gt; or read how we set up &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites?utm_source=hashnode&amp;amp;utm_medium=referral&amp;amp;utm_campaign=hashnode-monitor-maintain-laravel-apps-multiple-clients&quot;&gt;automated PageSpeed monitoring for multiple sites&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://forge.laravel.com/docs&quot;&gt;Laravel Forge documentation&lt;/a&gt; (Laravel)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.vapor.build/&quot;&gt;Laravel Vapor documentation&lt;/a&gt; (Laravel)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developers.google.com/speed/docs/insights/v5/about&quot;&gt;About PageSpeed Insights&lt;/a&gt; (Google)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/optimize-lcp&quot;&gt;Optimize Largest Contentful Paint&lt;/a&gt; (web.dev)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/articles/optimize-inp&quot;&gt;Optimize Interaction to Next Paint&lt;/a&gt; (web.dev)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/how-to-set-up-automated-pagespeed-monitoring-for-multiple-sites&quot;&gt;How to Set Up Automated PageSpeed Monitoring for Multiple Sites&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.com/blog/product-spotlight-multiple-client-sites-one-dashboard&quot;&gt;Product Spotlight: Managing Multiple Client Sites in One Dashboard&lt;/a&gt; (Apogee Watcher)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://apogeewatcher.hashnode.dev/monitor-maintain-laravel-apps-multiple-clients&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26305/how-to-monitor-and-maintain-laravel-apps-for-multiple-clients</guid>
<pubDate>Sun, 06 Sep 2026 07:20:36 +0000</pubDate>
</item>
<item>
<title>The Issues of Deploying Laravel on Dokploy: Database Connection Hell</title>
<link>https://coderlegion.com/24029/the-issues-of-deploying-laravel-on-dokploy-database-connection-hell</link>
<description>&lt;p&gt;If you’ve tried deploying a Laravel application on &lt;strong&gt;Dokploy&lt;/strong&gt; (the self-hosted Heroku alternative built on Docker) and immediately ran into a wall of &lt;code&gt;Database connection refused&lt;/code&gt; or &lt;code&gt;SQLSTATE[HY000] [2002]&lt;/code&gt; errors, you are not alone. &lt;/p&gt;
&lt;p&gt;It works perfectly on your local machine, but the moment it hits the server, it falls apart. Here is a real-world breakdown of the database connectivity issues we experienced, why they happened, and how to fix them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-localhost-trap-the-127-0-0-1-connection-refused-&quot;&gt;1. The &quot;Localhost&quot; Trap (The &lt;code&gt;127.0.0.1&lt;/code&gt; Connection Refused)&lt;/h3&gt;
&lt;h4 id=&quot;the-issue&quot;&gt;The Issue&lt;/h4&gt;
&lt;p&gt;On your local computer, you probably set your &lt;code&gt;.env&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;DB_HOST=127.0.0.1
DB_PORT=3306
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you deploy this to Dokploy, your Laravel application crashes on start because it cannot connect to the database.&lt;/p&gt;
&lt;h4 id=&quot;the-reason&quot;&gt;The Reason&lt;/h4&gt;
&lt;p&gt;Dokploy runs everything in isolated &lt;strong&gt;Docker Containers&lt;/strong&gt;. &lt;br&gt;
Inside a container, &lt;code&gt;127.0.0.1&lt;/code&gt; or &lt;code&gt;localhost&lt;/code&gt; refers to &lt;strong&gt;the Laravel container itself&lt;/strong&gt;, not the host machine or your database container. Since there is no database running &lt;em&gt;inside&lt;/em&gt; your Laravel code container, it refuses the connection.&lt;/p&gt;
&lt;h4 id=&quot;the-solution&quot;&gt;The Solution&lt;/h4&gt;
&lt;p&gt;You must use Docker's internal networking. &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If your database is created inside Dokploy, Dokploy assigns it a specific network service name (e.g. &lt;code&gt;dokploy-db-mysql&lt;/code&gt; or the service name you typed in the database creation wizard).&lt;/li&gt;
&lt;li&gt;Change your host in the Dokploy Environment Variables UI to match that service name:&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;DB_HOST=dokploy-db-mysql  # Use Dokploy's internal container name
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-config-caching-nightmare-why-changing-env-variables-in-the-ui-did-nothing-&quot;&gt;2. The Config Caching Nightmare (Why changing Env Variables in the UI did nothing)&lt;/h3&gt;
&lt;h4 id=&quot;the-issue-1&quot;&gt;The Issue&lt;/h4&gt;
&lt;p&gt;You realized the host was wrong, went to the Dokploy dashboard, updated the &lt;code&gt;DB_HOST&lt;/code&gt; environment variable to the correct container hostname, saved, and redeployed. &lt;br&gt;
Yet, the app &lt;em&gt;still&lt;/em&gt; threw the old connection error looking for &lt;code&gt;127.0.0.1&lt;/code&gt;. You checked the container logs, confirmed the new environment variables were set, but Laravel simply ignored them.&lt;/p&gt;
&lt;h4 id=&quot;the-reason-1&quot;&gt;The Reason&lt;/h4&gt;
&lt;p&gt;This happens if you run configuration caching in your &lt;code&gt;Dockerfile&lt;/code&gt; during the build step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN php artisan config:cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When Laravel builds your container, it runs &lt;code&gt;config:cache&lt;/code&gt; and &lt;strong&gt;bakes&lt;/strong&gt; whatever &lt;code&gt;.env&lt;/code&gt; values exist at build-time directly into a static cache file (&lt;code&gt;bootstrap/cache/config.php&lt;/code&gt;). &lt;br&gt;
At runtime, Laravel completely ignores your Dokploy UI environment variables and loads the baked-in values instead.&lt;/p&gt;
&lt;h4 id=&quot;the-solution-1&quot;&gt;The Solution&lt;/h4&gt;
&lt;p&gt;Never cache your configuration during the Docker build stage if you plan to change environment variables dynamically.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Remove &lt;code&gt;RUN php artisan config:cache&lt;/code&gt; from your build steps.&lt;/li&gt;
&lt;li&gt;If you want caching for speed in production, clear it on boot by running &lt;code&gt;php artisan config:clear&lt;/code&gt; or &lt;code&gt;php artisan optimize:clear&lt;/code&gt; inside your container's startup command / entrypoint script.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-the-race-condition-migrations-crashing-the-build-&quot;&gt;3. The Race Condition (Migrations crashing the build)&lt;/h3&gt;
&lt;h4 id=&quot;the-issue-2&quot;&gt;The Issue&lt;/h4&gt;
&lt;p&gt;You configured your Docker container to automatically run migrations on deployment:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan migrate --force
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But your deployment failed during this step because the database connection timed out or was refused, even though all credentials were 100% correct.&lt;/p&gt;
&lt;h4 id=&quot;the-reason-2&quot;&gt;The Reason&lt;/h4&gt;
&lt;p&gt;When Dokploy restarts or provisions your stack, it starts both the Laravel container and the MySQL/PostgreSQL container simultaneously.&lt;br&gt;
Laravel boots up in milliseconds and immediately tries to run migrations. However, MySQL is still executing its slow initial setup scripts, checking schemas, and opening sockets. The database container isn't ready to receive queries yet, so Laravel's migration command crashes, failing the entire Dokploy deployment.&lt;/p&gt;
&lt;h4 id=&quot;the-solution-2&quot;&gt;The Solution&lt;/h4&gt;
&lt;p&gt;You need to make Laravel &quot;wait&quot; for the database to boot. Add a small bash loop to your entrypoint script (&lt;code&gt;entrypoint.sh&lt;/code&gt;) that pings the database port before running migrations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &quot;Waiting for MySQL to boot...&quot;
until nc -z -v -w30 $DB_HOST $DB_PORT; do
  echo &quot;MySQL is not ready yet. Retrying in 2 seconds...&quot;
  sleep 2
done
echo &quot;MySQL is up! Running migrations...&quot;
php artisan migrate --force
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-port-mapping-confusion-internal-vs-external-ports-&quot;&gt;4. Port Mapping Confusion (Internal vs. External Ports)&lt;/h3&gt;
&lt;h4 id=&quot;the-issue-3&quot;&gt;The Issue&lt;/h4&gt;
&lt;p&gt;Mapping your Dokploy MySQL database port to &lt;code&gt;3306&lt;/code&gt; (or &lt;code&gt;3307&lt;/code&gt;) externally so you could connect via TablePlus/DBeaver from your laptop, but when configuring your Laravel &lt;code&gt;.env&lt;/code&gt; inside Dokploy, you weren't sure which port to use.&lt;/p&gt;
&lt;h4 id=&quot;the-reason-3&quot;&gt;The Reason&lt;/h4&gt;
&lt;p&gt;Docker has two layers of ports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Internal Port:&lt;/strong&gt; The port the service listens to inside the Docker private network (always &lt;code&gt;3306&lt;/code&gt; for MySQL).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External/Exposed Port:&lt;/strong&gt; The port mapped to the public server IP so you can connect from the outside world (e.g., mapping host &lt;code&gt;3307&lt;/code&gt; to container &lt;code&gt;3306&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inside Dokploy, your Laravel app and your MySQL container share the same private network. Therefore, they communicate &lt;strong&gt;internally&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;the-solution-3&quot;&gt;The Solution&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;Laravel (.env on Dokploy)&lt;/strong&gt;: Always use the internal port (&lt;code&gt;DB_PORT=3306&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;TablePlus/DBeaver (External laptop)&lt;/strong&gt;: Use the exposed host port (&lt;code&gt;3307&lt;/code&gt;) and the public server IP.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary-checklist-for-dokploy-deployments&quot;&gt;Summary Checklist for Dokploy deployments&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;DB_HOST&lt;/code&gt; to the internal Docker container/service name, not &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;DB_PORT&lt;/code&gt; to the internal port (&lt;code&gt;3306&lt;/code&gt; for MySQL), not the exposed host port.&lt;/li&gt;
&lt;li&gt;Avoid running &lt;code&gt;php artisan config:cache&lt;/code&gt; during Docker build time.&lt;/li&gt;
&lt;li&gt;Implement a &lt;code&gt;wait-for-it&lt;/code&gt; check in your startup script so migrations don't run before MySQL is fully booted.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24029/the-issues-of-deploying-laravel-on-dokploy-database-connection-hell</guid>
<pubDate>Mon, 03 Aug 2026 07:41:12 +0000</pubDate>
</item>
<item>
<title>Stop Slow Laravel: Your Next Architectural Leap Is Here!</title>
<link>https://coderlegion.com/21582/stop-slow-laravel-your-next-architectural-leap-is-here</link>
<description>
&lt;h2 id=&quot;stop-slow-laravel-your-next-architectural-leap-is-here&quot;&gt;Stop Slow Laravel: Your Next Architectural Leap Is Here!&lt;/h2&gt;

&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;

&lt;p&gt;It&amp;rsquo;s 2026, and the landscape of web development has evolved dramatically. If your Laravel applications are still relying on traditional PHP-FPM for request handling, you might be leaving significant performance on the table. The conversation around application speed has moved beyond mere query optimization; it&amp;rsquo;s an architectural paradigm shift. Enter Laravel Octane, a powerful package that integrates your Laravel application with high-performance application servers like RoadRunner or Swoole. This isn&amp;rsquo;t just about making PHP &amp;ldquo;faster&amp;rdquo;; it&amp;rsquo;s about fundamentally transforming how your application processes requests, ushering in an era of unprecedented throughput, reduced latency, and blazing-fast API responses. Octane transitions your application from a stateless, &amp;ldquo;boot-on-every-request&amp;rdquo; model to a persistent, long-running process, eliminating the costly framework bootstrap for each incoming call.&lt;/p&gt;

&lt;h3 id=&quot;code-layout-and-architectural-walkthrough&quot;&gt;Code Layout and Architectural Walkthrough&lt;/h3&gt;

&lt;p&gt;Embracing Laravel Octane demands a shift in mindset, particularly regarding application state. Let&amp;rsquo;s walk through the essential steps and considerations to integrate Octane into your project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Installation and Setup:&lt;/strong&gt;
Getting started with Octane is straightforward. You first install the package via Composer:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;composer require laravel/octane
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, you&amp;rsquo;ll install Octane into your application, choosing your preferred server driver (RoadRunner or Swoole). RoadRunner is often simpler to get started with due to fewer external dependencies for many environments.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;php artisan octane:install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command publishes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/octane.php&lt;/code&gt; configuration file, which allows you to fine-tune settings like the number of worker processes, maximum requests per worker before restarting, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The Persistent Server Model:&lt;/strong&gt;
Unlike FPM, which reboots your entire application for every request, Octane leverages a persistent application server. When you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;php artisan octane:start&lt;/code&gt;, a dedicated server process (RoadRunner or Swoole) is launched. This server loads your Laravel application once, keeping it in memory. Subsequent requests are then routed to this already-booted application instance. This fundamentally eliminates the overhead of bootstrapping the framework on &lt;em&gt;every single request&lt;/em&gt;, leading to dramatic performance improvements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Embracing the Stateful Mindset:&lt;/strong&gt;
This is the most critical aspect of adopting Octane. Because your application remains loaded in memory between requests, global state can persist, leading to unexpected behavior if not managed carefully.
Key areas to monitor:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Static Properties &amp;amp; Global Variables:&lt;/strong&gt; Any data stored in static properties or global variables will persist across requests. This can lead to data leaks or incorrect behavior if not reset.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service Container Bindings:&lt;/strong&gt; While Octane intelligently resets many core framework services, custom singleton bindings in your service container might retain state.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Opened Resources:&lt;/strong&gt; Database connections, file handles, or network sockets that are not properly closed might accumulate or become stale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Octane&amp;rsquo;s Mitigation Strategies:&lt;/strong&gt;
Octane provides powerful hooks to manage state:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Octane::onRequest&lt;/code&gt;:&lt;/strong&gt; You can register callbacks to be executed at the beginning of each request. This is ideal for manually resetting any custom global state or re-binding services if necessary.
    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Laravel\Octane\Facades\Octane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Clear any custom static caches&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;MyCustomService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[];&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// Rebind specific service if it holds request-specific state&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;$app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;bind&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SomeService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SomeService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Octane::onWorkerStopping&lt;/code&gt;:&lt;/strong&gt; This hook can be used for cleanup tasks when a worker process is gracefully shutting down.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automatic Resetting:&lt;/strong&gt; Octane intelligently resets many core Laravel components, such as request input, session data, authentication state, and even clears resolved instances from the service container (unless explicitly configured otherwise).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most well-architected Laravel applications that rely heavily on dependency injection and avoid excessive global state, the transition is smoother than anticipated. Focus on ensuring your services and controllers are stateless or properly reset if they must hold state.&lt;/p&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;Adopting Laravel Octane is no longer an edge-case optimization for the ultra-high-traffic elite; it&amp;rsquo;s becoming the baseline for modern, efficient Laravel applications. The architectural shift to a persistent application server, powered by RoadRunner or Swoole, fundamentally redefines performance. By eliminating per-request framework bootstrapping, Octane delivers 10x throughput, significantly reduced latency, and an incredibly responsive user experience. While it demands a careful consideration of application state, the performance gains and competitive advantage it provides are immense. Stop leaving raw performance on the table. Embrace Octane today and position your Laravel applications for peak efficiency and future success.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21582/stop-slow-laravel-your-next-architectural-leap-is-here</guid>
<pubDate>Wed, 01 Jul 2026 06:34:24 +0000</pubDate>
</item>
<item>
<title>Your Laravel Isn’t Slow, *Your Architecture Is*.</title>
<link>https://coderlegion.com/21413/your-laravel-isnt-slow-your-architecture-is</link>
<description>
&lt;h2 id=&quot;your-laravel-isnt-slow-your-architecture-is-mastering-scale-with-advanced-octane--reactive-caching&quot;&gt;Your Laravel Isn&amp;rsquo;t Slow, Your Architecture Is: Mastering Scale with Advanced Octane &amp;amp; Reactive Caching&lt;/h2&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;

&lt;p&gt;If your Laravel API is struggling under heavy load, the instinct is often to dive deep into optimizing Eloquent queries or micro-tweaking individual lines of code. While performance profiling remains crucial, this approach often addresses symptoms, not the root cause. The real bottleneck in high-traffic Laravel deployments isn&amp;rsquo;t typically the framework itself; it&amp;rsquo;s the architectural paradigm. We need to shift our focus from optimizing individual operations to optimizing the &lt;em&gt;flow&lt;/em&gt; and &lt;em&gt;state&lt;/em&gt; of our application. This tutorial will guide you through two advanced strategies &amp;ndash; leveraging Octane&amp;rsquo;s asynchronous capabilities and implementing a reactive caching strategy with Redis Streams &amp;ndash; to unlock sustained low-latency throughput that transforms your Laravel application into a true high-performance powerhouse.&lt;/p&gt;

&lt;h3 id=&quot;code-layout--architectural-walkthrough&quot;&gt;Code Layout &amp;amp; Architectural Walkthrough&lt;/h3&gt;

&lt;p&gt;Moving beyond basic Swoole or RoadRunner setups, we&amp;rsquo;re talking about a paradigm shift that integrates background processing directly into the request flow and introduces real-time cache invalidation.&lt;/p&gt;

&lt;h4 id=&quot;1-octanes-asynchronous-power-non-blocking-operations&quot;&gt;1. Octane&amp;rsquo;s Asynchronous Power: Non-Blocking Operations&lt;/h4&gt;

&lt;p&gt;The key here is to release the HTTP response &lt;em&gt;before&lt;/em&gt; non-critical operations complete, leveraging Octane&amp;rsquo;s persistent application state. Laravel&amp;rsquo;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dispatchAfterResponse()&lt;/code&gt; is the starting point, but we&amp;rsquo;ll integrate it with custom Octane bootloader hooks to ensure robust, non-blocking execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conceptual Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Custom Octane Bootloader:&lt;/strong&gt; Create a service provider that registers Octane worker lifecycle hooks. This allows you to perform setup and teardown tasks for each worker process, ensuring resources are ready.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Providers/OctaneServiceProvider.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Providers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Octane&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OctaneServiceProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceProvider&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onWorkerStart&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Perform expensive, one-time worker setup here&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// e.g., warm up a complex service container dependency&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

        &lt;span class=&quot;nc&quot;&gt;Octane&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;onWorkerError&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Throwable&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Log or handle worker errors gracefully&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Consider Octane::onWorkerStop for cleanup if necessary&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Asynchronous Dispatch in Controllers/Services:&lt;/strong&gt; In your critical API endpoints, identify operations that don&amp;rsquo;t &lt;em&gt;strictly&lt;/em&gt; need to block the user&amp;rsquo;s response (e.g., audit logging, sending non-critical notifications, syncing data to a third-party service).&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Http/Controllers/OrderController.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Http\Controllers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Jobs\ProcessAuditLog&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Jobs\SyncOrderToCRM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Routing\Controller&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;OrderController&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Controller&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Request&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// ... (Validate request, create order, etc.) ...&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;());&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Dispatch non-critical jobs to the queue *after* response is sent&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// This leverages Octane's persistent connections without blocking the user.&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;ProcessAuditLog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Order Created'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;afterResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SyncOrderToCRM&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dispatch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;afterResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'message'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Order created successfully!'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'order_id'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$order&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;201&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&amp;gt;afterResponse()&lt;/code&gt;, Laravel ensures these jobs are pushed to your configured queue (e.g., Redis, database) only &lt;em&gt;after&lt;/em&gt; the HTTP response has been fully sent to the client. Octane&amp;rsquo;s persistent workers ensure that the queue workers are readily available and efficient, minimizing latency overhead for job dispatching.&lt;/p&gt;

&lt;h4 id=&quot;2-reactive-caching-with-redis-streams-for-real-time-invalidation&quot;&gt;2. Reactive Caching with Redis Streams for Real-time Invalidation&lt;/h4&gt;

&lt;p&gt;Simple TTL-based caching often leads to stale data or necessitates aggressive, short TTLs that negate performance benefits. A reactive caching strategy uses Redis Streams to broadcast data changes and trigger real-time invalidation across your data layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conceptual Implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Publisher (Model Event Listener):&lt;/strong&gt; Whenever a critical data model is updated, deleted, or created, an event is published to a Redis Stream.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Providers/AppServiceProvider.php (or a dedicated EventServiceProvider)&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Providers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Models\Product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Services\CacheInvalidationService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\ServiceProvider&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AppServiceProvider&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceProvider&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;boot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CacheInvalidationService&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;created&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_updated'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;updated&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_updated'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;deleted&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheInvalidationService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'product_deleted'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$product&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// app/Services/CacheInvalidationService.php&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Services&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CacheInvalidationService&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidation_stream'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;array&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xadd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'*'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'event'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'entity_id'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'timestamp'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;timestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;s1&quot;&gt;'data'&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;json_encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Consumer (Queue Worker/Octane Worker):&lt;/strong&gt; A dedicated listener consumes this Redis Stream. Upon receiving an event, it knows exactly which cache keys (for complex objects or query results) need to be invalidated.&lt;/p&gt;

    &lt;div class=&quot;language-php highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/Jobs/ProcessCacheInvalidationStream.php (a long-running Octane-aware consumer)&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;namespace&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;App\Jobs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;App\Services\CacheManagerService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Manages complex cache keys&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Bus\Queueable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Foundation\Bus\Dispatchable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Queue\InteractsWithQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Queue\SerializesModels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Redis&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Illuminate\Support\Facades\Log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ProcessCacheInvalidationStream&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ShouldQueue&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Dispatchable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;InteractsWithQueue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Queueable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SerializesModels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;handle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;CacheManagerService&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$cacheManagerService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidation_stream'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'cache_invalidator_group'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;nv&quot;&gt;$consumerName&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;gethostname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'-'&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;uniqid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// Unique consumer name&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Create consumer group if it doesn't exist&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xgroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'CREATE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'$'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'MKSTREAM'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// Group likely already exists&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;c1&quot;&gt;// Continuously read from the stream&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Redis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;xreadgroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'GROUP'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerGroup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$consumerName&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'BLOCK'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'COUNT'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'STREAMS'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&amp;gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;

            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$messages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$streamKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;$event&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$message&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'event'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt;
                    &lt;span class=&quot;nv&quot;&gt;$entityId&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;int&lt;/span&gt;&amp;lt;span cl&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/li&gt;&lt;/ol&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/21413/your-laravel-isnt-slow-your-architecture-is</guid>
<pubDate>Fri, 26 Jun 2026 13:35:52 +0000</pubDate>
</item>
<item>
<title>How to Build a Multi-Department Approval Workflow in Laravel</title>
<link>https://coderlegion.com/21141/how-to-build-a-multi-department-approval-workflow-in-laravel</link>
<description>&lt;p&gt;TL;DR: Step-by-step guide to building a multi-department approval workflow in Laravel with database-driven routing, Events, and role-based access. Code included.&lt;/p&gt;
&lt;p&gt;Introduction&lt;br&gt;
One of the most common problems I saw while building VMMS was this: documents get created, emailed to the next department, sit in someone's inbox for days, and when management asks &quot;where's that voucher?&quot; — nobody knows.&lt;br&gt;
In this article I'll walk you through how I built a flexible multi-department approval workflow in Laravel that solves exactly this problem.&lt;/p&gt;
&lt;p&gt;The Core Concept&lt;br&gt;
Instead of hardcoding approval steps, I built a pipeline where each document has a current stage, and the next stage is determined by a department configuration stored in the database — not in the code.&lt;br&gt;
This means you can add, remove, or reorder departments without touching a single line of PHP.&lt;/p&gt;
&lt;p&gt;Database Structure&lt;br&gt;
Here are the key tables:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// vouchers table
Schema::create('vouchers', function (Blueprint $table) {
    $table-&amp;gt;id();
    $table-&amp;gt;string('title');
    $table-&amp;gt;foreignId('current_department_id')-&amp;gt;constrained('departments');
    $table-&amp;gt;enum('status', ['ongoing', 'accomplished', 'rejected']);
    $table-&amp;gt;timestamps();
});

// departments table
Schema::create('departments', function (Blueprint $table) {
    $table-&amp;gt;id();
    $table-&amp;gt;string('name');
    $table-&amp;gt;integer('order'); // determines routing sequence
    $table-&amp;gt;timestamps();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Routing Logic&lt;br&gt;
When a staff member approves a voucher, it automatically moves to the next department based on the order column:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function approve(Voucher $voucher)
{
    $currentOrder = $voucher-&amp;gt;department-&amp;gt;order;

    $nextDepartment = Department::where('order', '&amp;gt;', $currentOrder)
        -&amp;gt;orderBy('order')
        -&amp;gt;first();

    if ($nextDepartment) {
        $voucher-&amp;gt;update([
            'current_department_id' =&amp;gt; $nextDepartment-&amp;gt;id,
            'status' =&amp;gt; 'ongoing'
        ]);
    } else {
        // No more departments — voucher is complete
        $voucher-&amp;gt;update(['status' =&amp;gt; 'accomplished']);
    }

    // Fire event for email notification
    event(new VoucherAdvanced($voucher));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Email Notifications&lt;br&gt;
I used Laravel Events and Listeners to trigger email notifications every time a voucher moves to the next stage:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// VoucherAdvanced Event
class VoucherAdvanced
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(public Voucher $voucher) {}
}

// Listener
class SendVoucherNotification implements ShouldQueue
{
    public function handle(VoucherAdvanced $event): void
    {
        $voucher = $event-&amp;gt;voucher;
        $staff = $voucher-&amp;gt;department-&amp;gt;staff;

        foreach ($staff as $member) {
            Mail::to($member-&amp;gt;email)-&amp;gt;send(new VoucherNotification($voucher));
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Role-Based Access&lt;br&gt;
Each department only sees vouchers assigned to them using Laravel Policies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function view(User $user, Voucher $voucher): bool
{
    return $user-&amp;gt;department_id === $voucher-&amp;gt;current_department_id
        || $user-&amp;gt;role === 'admin';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Real-Time Pipeline Tracker&lt;br&gt;
To show users where a voucher is in real time, I built a simple status tracker on the Vue 3 frontend that reads the current department and renders it as a progress indicator — no websockets needed, just a clean Inertia.js page refresh on action.&lt;/p&gt;
&lt;p&gt;What I Learned&lt;/p&gt;
&lt;p&gt;Keep routing logic in the database, not the code — it makes the system far more flexible&lt;br&gt;
Laravel Events are perfect for notifications — clean, queueable, and easy to extend&lt;br&gt;
Policies over middleware for row-level access — much cleaner when you have complex role rules&lt;/p&gt;
&lt;p&gt;Conclusion&lt;br&gt;
This pattern works for any multi-step approval system — not just vouchers. You could use it for leave requests, purchase orders, or any document that needs to pass through multiple departments.&lt;/p&gt;
&lt;p&gt;Want to see this in action?&lt;br&gt;
I built VMMS — a complete Voucher Management &amp;amp; Monitoring System using everything covered in this article.&lt;br&gt;
 Live demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://vmms-app-production.up.railway.app/login&quot;&gt;https://vmms-app-production.up.railway.app/login&lt;/a&gt;&lt;br&gt;
 Get the full source code: &lt;a rel=&quot;nofollow&quot; href=&quot;https://getvmms.gumroad.com/l/zeroqz&quot;&gt;https://getvmms.gumroad.com/l/zeroqz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy to answer any questions in the comments! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21141/how-to-build-a-multi-department-approval-workflow-in-laravel</guid>
<pubDate>Mon, 22 Jun 2026 12:50:47 +0000</pubDate>
</item>
<item>
<title>How I Built a Production-Ready Voucher Management System with Laravel 12 and Vue 3</title>
<link>https://coderlegion.com/21140/how-i-built-a-production-ready-voucher-management-system-with-laravel-12-and-vue-3</link>
<description>&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I'm a BS Information Technology student in the Philippines. During my time observing how &lt;br&gt;
offices work, I noticed something that kept bothering me.&lt;/p&gt;
&lt;p&gt;Voucher processing is still done manually almost everywhere. &lt;br&gt;
Someone fills out a paper form, hands it to the next office, &lt;br&gt;
that office stamps it and passes it again, and so on — until &lt;br&gt;
it either gets done or gets lost on someone's desk.&lt;/p&gt;
&lt;p&gt;No tracking. No visibility. The client has no idea where their &lt;br&gt;
request is. The admin has no data. Staff have no accountability.&lt;/p&gt;
&lt;p&gt;I kept thinking — this is exactly the kind of problem software &lt;br&gt;
should solve. So I spent the last few months building it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;VMMS stands for Voucher Management &amp;amp; Monitoring System.&lt;/p&gt;
&lt;p&gt;It handles the entire voucher request process — from the moment &lt;br&gt;
a client submits a request, all the way through each department &lt;br&gt;
that needs to process it, until it's finally released.&lt;/p&gt;
&lt;p&gt;No paper. No manual routing. No &quot;what's the status of my request?&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tech-stack&quot;&gt;Tech Stack&lt;/h3&gt;
&lt;p&gt;I went with what I know best:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Laravel 12&lt;/strong&gt; for the backend — routing, queues, and email jobs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue 3 (Composition API)&lt;/strong&gt; for the frontend — reactive and clean&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inertia.js v2&lt;/strong&gt; — so I don't have to build a separate API&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailwind CSS + Vuetify 3&lt;/strong&gt; — for UI components and styling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL 8.0&lt;/strong&gt; — for the relational data and complex queries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resend&lt;/strong&gt; — for transactional emails&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-it-can-do&quot;&gt;What It Can Do&lt;/h3&gt;
&lt;p&gt;There are three separate panels depending on your role:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Admin&lt;/strong&gt;&lt;br&gt;
You get full control — manage users, voucher types, and see &lt;br&gt;
everything through an analytics dashboard. There's a staff &lt;br&gt;
performance leaderboard, rejection rate charts, audit logs, &lt;br&gt;
and even custom branding options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Staff&lt;/strong&gt;&lt;br&gt;
Each department gets their own queue of requests to process. &lt;br&gt;
You can complete, reject, or flag a request for missing documents. &lt;br&gt;
There's also a personal performance dashboard showing your stats &lt;br&gt;
and ratings from clients.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;br&gt;
You submit requests online, track which department is currently &lt;br&gt;
processing your voucher in real time, upload corrected documents &lt;br&gt;
if something gets flagged, and rate the staff after it's done.&lt;/p&gt;
&lt;p&gt;Some other things worth mentioning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Deadlines are holiday-aware — staff don't get penalized for &lt;br&gt;
non-working days&lt;/li&gt;
&lt;li&gt;Every action is logged in an audit trail&lt;/li&gt;
&lt;li&gt;PDF and Excel exports on all reports&lt;/li&gt;
&lt;li&gt;Automated email notifications at every stage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-parts-that-were-actually-hard&quot;&gt;The Parts That Were Actually Hard&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Date queries across MySQL and MariaDB&lt;/strong&gt;&lt;br&gt;
I originally wrote date queries using MySQL-specific functions &lt;br&gt;
like MONTH() and MONTHNAME(). Broke on MariaDB. Had to rewrite &lt;br&gt;
everything to use Carbon instead — which honestly ended up &lt;br&gt;
being cleaner anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The pipeline tracker&lt;/strong&gt;&lt;br&gt;
Getting the pipeline stepper to accurately show which department &lt;br&gt;
is currently processing a request was trickier than I expected. &lt;br&gt;
Especially edge cases — what if a department is skipped? What if &lt;br&gt;
a request gets returned for missing documents mid-flow? Took a &lt;br&gt;
few rewrites to get right.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Holiday-aware deadlines&lt;/strong&gt;&lt;br&gt;
I built a DateHelpers service that calculates business days &lt;br&gt;
while excluding weekends and holidays. Sounds simple, took way &lt;br&gt;
longer than expected. Really happy with how it turned out though.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt; Live demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://vmms-app-production.up.railway.app/login&quot;&gt;https://vmms-app-production.up.railway.app/login&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Credentials if you want to explore:&lt;/p&gt;
&lt;p&gt;Admin: admin@vmms.demo | Password: Admin@Demo123!&lt;br&gt;
Staff:  staff.infra@vmms.demo | staff.budget@vmms.demo | staff.procurement@vmms.demo | staff.supply@vmms.demo | staff.executive@vmms.demo | Password: Staff@Demo123!&lt;br&gt;
Client: client@vmms.demo | Password: Client@Demo123!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;it-s-available-on-gumroad&quot;&gt;It's Available on Gumroad&lt;/h3&gt;
&lt;p&gt;I'm selling VMMS in two versions:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Obfuscated / Protected — $150&lt;/strong&gt;&lt;br&gt;
Deploy it as-is. PHP backend is protected, Vue frontend and &lt;br&gt;
config files are fully readable. Good for offices and &lt;br&gt;
institutions that just want to run it.&lt;/p&gt;
&lt;p&gt;‍ &lt;strong&gt;Full Source Code — $400&lt;/strong&gt;&lt;br&gt;
Every file is readable and editable. Good for developers who &lt;br&gt;
want to customize it for a specific client or use case.&lt;/p&gt;
&lt;p&gt;Both versions come with documentation, a quick start guide, &lt;br&gt;
and deployment configs for Nginx, Apache, and Railway.&lt;/p&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://getvmms.gumroad.com/l/zeroqz&quot;&gt;https://getvmms.gumroad.com/l/zeroqz&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Happy to answer questions in the comments — whether about &lt;br&gt;
the build, the tech stack, or anything else. &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21140/how-i-built-a-production-ready-voucher-management-system-with-laravel-12-and-vue-3</guid>
<pubDate>Mon, 22 Jun 2026 12:50:14 +0000</pubDate>
</item>
<item>
<title>Through the Looking Glass: Why Your Server Logs Are a Playground for Cyber-Detectives</title>
<link>https://coderlegion.com/20312/through-the-looking-glass-why-your-server-logs-are-a-playground-for-cyber-detectives</link>
<description>&lt;p&gt;When you deploy a custom analytics package or any traffic-monitoring tool, your expectations are usually modest: a few hits from friends, maybe your mom, or an occasional lost translator bot. But after the server runs for a couple of days, the reality often turns out to be significantly more complex.&lt;/p&gt;
&lt;p&gt;Recently, while auditing the logs of my Laravel-based analytics package, I discovered that my project had become a focal point for researchers who don't just &quot;scroll the feed,&quot; but dissect the internet down to its molecular level.&lt;/p&gt;
&lt;p&gt;As a developer, it made me curious: what exactly are these &quot;guests from the future&quot; (like Firefox 140.0) looking for, and why are corporate entities like Palo Alto Networks suddenly keeping such a close eye on a domain that barely existed yesterday?&lt;/p&gt;
&lt;h3 id=&quot;when-grey-traffic-becomes-an-object-of-study&quot;&gt;When &quot;Grey&quot; Traffic Becomes an Object of Study&lt;/h3&gt;
&lt;p&gt;The first thing that stands out in the logs isn't the usual bot noise, but Power Users. I’m talking about visitors coming via IPv6 tunnels, using Linux, and identifying as DuckDuckGo users. These are individuals who consciously avoid tracking and operate with tools that most of us consider &quot;advanced&quot; or &quot;niche.&quot;&lt;/p&gt;
&lt;h4 id=&quot;they-behave-like-professional-auditors-&quot;&gt;They behave like professional auditors:&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Protocol Adherence:&lt;/strong&gt; Checking robots.txt before crawling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jurisdictional Assessment:&lt;/strong&gt; Analyzing Terms of Service within seconds (a classic professional trait: understand the jurisdiction before pulling the trigger).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Intentional Focus:&lt;/strong&gt; Bypassing general content in favor of direct interaction with security tools (like DNS audits).&lt;/p&gt;
&lt;p&gt;This is a vital signal for any engineer: if your software attracts this type of user, you've built something genuinely practical. Simultaneously, it serves as a high-stakes stress test for your entire stack.&lt;/p&gt;
&lt;h3 id=&quot;dns-detective-the-karachi-police-case&quot;&gt;DNS Detective: The &quot;Karachi Police&quot; Case&lt;/h3&gt;
&lt;p&gt;The situation becomes truly fascinating when your tool starts being used as a &quot;proxy scanner&quot; to probe other resources. For instance, I noticed requests hitting karachipolice.gov.pk via my tool. Using an independent auditor to probe a government domain is a perfect example of how borders vanish on the web.&lt;/p&gt;
&lt;p&gt;What did the audit reveal, and what should you look for in your own projects?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DMARC: null&lt;/code&gt;. An absent protection policy is an open invitation for attackers to perform email spoofing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SPF ~all&lt;/code&gt;. A &quot;soft&quot; lock that exists formally but fails to effectively block unauthorized senders.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;IPv6 Facade&lt;/code&gt;. A site may correctly resolve via modern &lt;code&gt;AAAA&lt;/code&gt; records, but still rely on an outdated, vulnerable mail infrastructure behind the scenes.&lt;/p&gt;
&lt;p&gt;This case serves as a sharp reminder: &lt;strong&gt;domain security isn't about wall thickness; it's about the precision of a single DNS record.&lt;/strong&gt; If you haven't audited your own domain's configuration in a while, it is high time to verify those settings — you can run a &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.net/security-tools/dns-records-check&quot;&gt;free DNS audit&lt;/a&gt; to ensure your records are as airtight as they need to be.&lt;/p&gt;
&lt;h3 id=&quot;corporate-ghosts-from-palo-alto-networks&quot;&gt;Corporate &quot;Ghosts&quot; from Palo Alto Networks&lt;/h3&gt;
&lt;p&gt;While the first group represents human researchers, Palo Alto Networks and their peers represent the world of automated global threat mapping. Once your service begins exhibiting behavior resembling a network scanner, it inevitably ends up on corporate radar.&lt;/p&gt;
&lt;h4 id=&quot;for-a-developer-this-is-a-critical-turning-point-&quot;&gt;For a developer, this is a critical turning point:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your domain reputation is being built automatically.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your service behaves &quot;professionally&quot; (Laravel 12, clean headers, valid configurations), algorithms flag you as a legitimate research node, which improves your trust rating across the &quot;clean internet.&quot;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;final-thoughts-what-should-we-learn-&quot;&gt;Final Thoughts: What Should We Learn?&lt;/h3&gt;
&lt;p&gt;Analytics is more than just traffic charts; it is a powerful tool for code hygiene and security. When you read your logs, you are reading the story of the web in real-time.&lt;/p&gt;
&lt;p&gt;If you are interested in the technical implementation details of how I built this package without crashing production, or want to dive deeper into the security of HTTP headers (and why a single forgotten comma in your CSP can &quot;kill&quot; your site for Googlebot), check out these resources:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production&quot;&gt;How I Built My Own Laravel Analytics Package (and Almost Didn't Crash Production)&lt;/a&gt; — Technical implementation details.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/your-digital-fortress-why-a-security-headers-audit-is-essential&quot;&gt;Your Digital Fortress: Why a Security Headers Audit is Essential&lt;/a&gt; — Why headers are the first thing every &quot;pro&quot; checks.&lt;/p&gt;
&lt;p&gt;Take care of your HTTP headers, and don't forget to check your logs. They are always more interesting than Google Analytics.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20312/through-the-looking-glass-why-your-server-logs-are-a-playground-for-cyber-detectives</guid>
<pubDate>Thu, 11 Jun 2026 13:54:28 +0000</pubDate>
</item>
<item>
<title>Laravel AI Agents Now Support MCP Servers</title>
<link>https://coderlegion.com/20106/laravel-ai-agents-now-support-mcp-servers</link>
<description>&lt;p&gt;Laravel AI SDK now supports MCP (Model Context Protocol) servers through a dedicated MCP client in the laravel/mcp package. Developers can connect Laravel AI agents to any MCP server — local via STDIO or remote via HTTP — and use its tools alongside custom agent tools. Authentication is handled via bearer tokens or full OAuth (including PKCE and client credentials). The integration is designed so MCP tools drop into an agent's existing tools() method without changing the agent loop. Additional features include built-in caching for tool lists and a faking/testing layer that lets you test MCP-powered agents without a live server.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20106/laravel-ai-agents-now-support-mcp-servers</guid>
<pubDate>Tue, 09 Jun 2026 18:29:18 +0000</pubDate>
</item>
<item>
<title>Building a Digital Customs Checkpoint: My 7-Part Journey in Laravel Analytics &amp; Bot Defense</title>
<link>https://coderlegion.com/19648/building-a-digital-customs-checkpoint-my-7-part-journey-in-laravel-analytics-bot-defense</link>
<description>&lt;p&gt;Over the past year, developing &lt;code&gt;laravel-visit-analytics&lt;/code&gt; has evolved from a simple experiment into a significant engineering challenge. When standard analytics tools failed to meet my requirements for GDPR/DSGVO compliance and data accuracy, I realized: if you want it done right, do it yourself.&lt;/p&gt;
&lt;p&gt;This 7-part series chronicles my transition from naive trust in browser headers to building a system that detects botnets in real-time based on their unique &quot;digital fingerprints.&quot;&lt;/p&gt;
&lt;h3 id=&quot;project-roadmap-&quot;&gt;Project Roadmap:&lt;/h3&gt;
&lt;h4 id=&quot;phase-1-architecture-amp-foundation&quot;&gt;Phase 1: Architecture &amp;amp; Foundation&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chapter 1: Breaking Free from the Giants.&lt;/strong&gt; Why I abandoned Google Analytics for a Privacy-First approach — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production&quot;&gt;How I Built My Own Laravel Analytics Package (and Almost Didn't Crash Production)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chapter 2: Refactoring 2.0.&lt;/strong&gt; Replacing heavy middleware with an elegant Chain of Responsibility and optimizing database performance — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/refactoring-laravel-visit-analytics-the-path-to-version-200&quot;&gt;Refactoring Laravel Visit Analytics: The Path to Version 2.0.0&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;phase-2-the-logic-of-suspicion&quot;&gt;Phase 2: The Logic of Suspicion&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chapter 3: The Lie Detector.&lt;/strong&gt; Implementing a scoring system, Snowball Effect, and honeypots to filter out &quot;junk&quot; traffic — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/a-lie-detector-for-http-requests-analytics-through-time&quot;&gt;A Lie Detector for HTTP Requests: Analytics Through Time&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chapter 4: Through the Looking Glass.&lt;/strong&gt; How DNS auditing and IPv6 analysis helped uncover true traffic sources — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/through-the-looking-glass-of-logs-karachi-police-duckduckgo-and-ipv6-magic&quot;&gt;Through the Looking Glass of Logs: Karachi Police, DuckDuckGo, and IPv6 Magic&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chapter 5: Code with a Human Face.&lt;/strong&gt; The story of how I almost banned my son's vintage Sony Ericsson and why I implemented a &quot;mercy coefficient&quot; — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/human-centric-bot-detection-lessons-from-sony-ericsson-and-win98&quot;&gt;Human-Centric Bot Detection: Lessons from Sony Ericsson and Win98&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;phase-3-advanced-defense&quot;&gt;Phase 3: Advanced Defense&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Chapter 6: Hunting Digital Chameleons (Part 1).&lt;/strong&gt; Defeating botnets via Client Hints and implementing real-time anonymization in v2.4.0 — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/hunting-digital-chameleons-how-we-defeated-botnets-in-laravel-v240&quot;&gt;Hunting Digital Chameleons: How We Defeated Botnets in Laravel v2.4.0&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chapter 7: Hunting Digital Chameleons (Part 2).&lt;/strong&gt; Why session imitation is a bot's weak point and how I detect anomalies in session stability — &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/hunting-digital-chameleons-why-session-imitation-is-the-weakest-link-for-modern-bots&quot;&gt;Hunting Digital Chameleons: Why Session Imitation Is the Weakest Link for Modern Bots&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;What’s Next?&lt;/strong&gt;&lt;br&gt;
The foundation is set. I am currently focusing on data integrity, but plans are already in motion to visualize these threat patterns directly in the Laravel Filament dashboard. Stay tuned!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19648/building-a-digital-customs-checkpoint-my-7-part-journey-in-laravel-analytics-bot-defense</guid>
<pubDate>Thu, 04 Jun 2026 09:00:00 +0000</pubDate>
</item>
<item>
<title>How I Built My Own Laravel Analytics Package (and Almost Didn't Crash Production)</title>
<link>https://coderlegion.com/19555/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production</link>
<description>&lt;h3 id=&quot;why-not-google-analytics-or-why-i-love-reinventing-the-wheel-&quot;&gt;Why Not Google Analytics? (Or why I love reinventing the wheel)&lt;/h3&gt;
&lt;p&gt;To be honest, this wasn't an easy call. I’ve been in development for quite a while and had grown accustomed to GA. What could be simpler: you slap a script with your ID onto the site, and data starts flowing into the analytics console. All that's left is to wait for the traffic to roll in and then analyze it by country, gender, age groups, and so on.&lt;/p&gt;
&lt;p&gt;Yes, that’s how it used to be, but in today’s reality, there are objective reasons to rethink this concept. Let’s be real: hooking up Google Analytics in 2026 is like putting a massive deadbolt on your front door but leaving the keys with your neighbor. Everything seems under control, but the neighbor knows exactly when you came home, what you bought, and why you have a long face. And when someone visits you and wants to stay incognito, the neighbor won't give them the keys, and they won't even tell you they stopped by. Their response would be something like: “Nobody came, I never sleep, everything is under control…”. What am I getting at?&lt;/p&gt;
&lt;h4 id=&quot;remember-the-general-data-protection-regulation-gdpr-&quot;&gt;Remember the General Data Protection Regulation (GDPR)?&lt;/h4&gt;
&lt;p&gt;Now, for your analytics to work, you must display a cookie consent banner and get the user's permission. And that’s where the problem lies. 90% of users don’t accept all cookies—only the strictly necessary ones. And what does that mean? Google Analytics ends up dead in the water. Besides, everyone is sick and tired of these banners. So, if there’s a legal way to ditch them, why not take it?&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;(To learn how to avoid legal trouble with data protection laws, check out the article &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.dev/en/blog/gdpr-without-the-headache-a-guide-for-web-developers-in-germany&quot;&gt;GDPR Without the Headache: A Guide for Web Developers in Germany &lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The decision to drop GA was made. When I decided to hit the &quot;Eject&quot; button and catapult myself out of the Google ecosystem, I faced a logical question: what would fill the void? Because I still needed the numbers. I started looking at the heavy artillery.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The first candidate was Matomo (formerly Piwik):&lt;/em&gt; Probably the most powerful all-in-one machine. It’s like keeping a pet elephant in your backyard. It does everything, the database grows like crazy, but it requires a separate PHP server, MySQL, and constant babysitting. For my small pet projects, it felt like trying to drive nails with a microscope.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The second tool I looked at was Plausible / Fathom:&lt;/em&gt; Sleek, modern, and privacy-respecting. But there’s a catch: you either pay a subscription (a questionable investment for a free tool) or you mess around with self-hosted Docker versions, which also eat up a good chunk of your VPS RAM.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I looked at all of this and thought: “Do I really need to spin up an entire infrastructure just to know that 50 people read my article on German taxes yesterday?”. That’s when it hit me: I don’t need a &quot;combine harvester.&quot; I need a tiny, precision scalpel that lives right inside my Laravel application.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wanted something of my own: lightweight, like a morning espresso, and not asking annoying questions about GDPR. Plus, I was simply curious about who all these people (and bots) were &quot;knocking&quot; on my security tools at &lt;a rel=&quot;nofollow&quot; href=&quot;https://oleant.net&quot;&gt;oleant.net&lt;/a&gt;. Since this blog also needed the same tool, I decided to develop a standalone package that could be easily installed via composer and published openly on Packagist.&lt;/p&gt;
&lt;p&gt;For those interested in diving into the code, the package is called &lt;strong&gt;&lt;code&gt;oleant/laravel-visit-analytics&lt;/code&gt;&lt;/strong&gt;, compatible with Laravel versions 10/11/12. GitHub link: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Oleant-NET/laravel-visit-analytics&quot;&gt;https://github.com/Oleant-NET/laravel-visit-analytics&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-heart-of-the-system-middleware-and-the-magic-of-the-aftertaste&quot;&gt;The Heart of the System: Middleware and the Magic of the Aftertaste&lt;/h4&gt;
&lt;p&gt;Which architecture to use? There are various ways to implement this. Fortunately, Laravel has a great Middleware mechanism. I decided to stick with that, but would it slow down the user? They shouldn't have to care about my overhead. And what if the database goes down or something goes wrong? A 500 error page as the face of the site is definitely not what I was aiming for.&lt;/p&gt;
&lt;p&gt;That’s why a crucial decision was made — to use the &lt;strong&gt;&lt;code&gt;terminate()&lt;/code&gt;&lt;/strong&gt; method. In a Laravel Middleware, it’s like a polite waiter: he brings you the check and smiles (the &lt;strong&gt;&lt;code&gt;handle()&lt;/code&gt;&lt;/strong&gt; method), and only after you’ve already left the restaurant does he go back to wipe the table and log the tip (the &lt;strong&gt;&lt;code&gt;terminate()&lt;/code&gt;&lt;/strong&gt; method).&lt;/p&gt;
&lt;p&gt;The user has already received their page and is happy, while our server quietly and without rush writes the logs to the database in that moment. Even if something goes wrong, the client still leaves satisfied, and the waiter... just doesn't get a tip this time. Just kidding, but here’s how it works in practice:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function terminate(Request $request, Response $response): void
{
    try {
        // Logic to exclude non-target clients, like Admin etc.
        // …
        // Next, write our visitors to the database
        $this-&amp;gt;logVisit($request);
    } catch (\Throwable $e) {
        // If the DB takes a nap, we won't wake the user with a 500 error
        \Log::error(&quot;Analytics failed, but we're keeping our cool: &quot; . $e-&amp;gt;getMessage());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Lifehack&lt;/strong&gt;: All analytics code must be wrapped in a try-catch. Believe me, there's nothing sillier than &quot;crashing&quot; an entire project just because the logger didn't have enough room for a long User-Agent or some other non-obvious case.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GDPR on the Fly&lt;/strong&gt;: How Not to Become Public Enemy No. 1&lt;br&gt;
To avoid slapping a banner the size of half the screen saying “We are watching you, bro,” I implemented IP anonymization. We simply trim the last part of the address before it ever touches the database. This anonymizes the user and fully complies with the law. Yet, we can still tell which country, data center, etc., the visit came from. We’ll talk more about data center bots once there's more data to show.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Back to IP anonymization:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;// Before: 192.168.1.154 -&amp;gt; After: 192.168.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s like seeing a crowd in masks: you understand that 5 people showed up, but who among them is your neighbor — you have no clue. The law is satisfied, and so is my conscience.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Payload:&lt;/strong&gt; Gathering Only the Goodies&lt;br&gt;
Initially, I wanted to write everything that comes in the URL to the database. But then I looked at the Livewire logs, where half the state of the planet is passed in the parameters, and realized — the database would explode. So, I decided to implement filtering based on allowed parameters in the config using &lt;strong&gt;&lt;code&gt;array_intersect_key&lt;/code&gt;&lt;/strong&gt;. Now, only what I’ve personally authorized in the config ends up in the logs. Clean, orderly, and zero fluff.&lt;/p&gt;
&lt;p&gt;The default set in the package config looks like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;'whitelist' =&amp;gt; [
    'utm_source',
    'utm_medium',
    'utm_campaign',
    'utm_term',
    'utm_content',
    'ref',
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But you can, of course, change it by publishing the config in your project first:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bash&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;php artisan vendor:publish --tag=&quot;visit-analytics-config&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After that, a visit-analytics.php file will appear in your config folder. There, you can not only expand the list of tracked parameters (for example, adding something like page or search) but also specify excluded paths so you don't turn your database into a dump for admin panel or technical endpoint logs.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h3&gt;
&lt;p&gt;The package started humming, and the data began to flow. I closed my laptop and went to bed, thinking I’d wake up to some visitor charts from a couple of friends. But reality turned out to be much tougher (and more interesting).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In the next episode, we’ll step through the &quot;looking glass&quot;: who are Palo Alto Networks, why do bots read my Terms of Service in 7 seconds, and why Linux + DuckDuckGo is a badge of quality for a visitor?&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19555/how-i-built-my-own-laravel-analytics-package-and-almost-didnt-crash-production</guid>
<pubDate>Wed, 03 Jun 2026 10:22:39 +0000</pubDate>
</item>
<item>
<title>Building A Production-Ready Laravel Google Sheets Package From Import To Test Suite</title>
<link>https://coderlegion.com/19417/building-a-production-ready-laravel-google-sheets-package-from-import-to-test-suite</link>
<description>&lt;p&gt;Google Sheets is often where business workflows begin.&lt;/p&gt;
&lt;p&gt;For Laravel apps, that usually means imports, exports, internal dashboards, user-managed spreadsheets, and the occasional &quot;can we sync this automatically?&quot; request.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;olamilekan/laravel-google-sheets&lt;/code&gt; &lt;code&gt;v1.1.0&lt;/code&gt; adds several features for those real-world workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Laravel 13 support&lt;/li&gt;
&lt;li&gt;Import diff previews&lt;/li&gt;
&lt;li&gt;Model, CSV, API, and two-way sync methods&lt;/li&gt;
&lt;li&gt;Dry-run sync command&lt;/li&gt;
&lt;li&gt;Human-friendly validation error sheets&lt;/li&gt;
&lt;li&gt;Retry and exponential backoff for temporary API failures&lt;/li&gt;
&lt;li&gt;Better testing fakes&lt;/li&gt;
&lt;li&gt;A GitHub Actions test matrix&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a backward-compatible feature release with 17 commits since &lt;code&gt;v1.0.0&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require olamilekan/laravel-google-sheets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Publish the config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan vendor:publish --tag=google-sheets-config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;laravel-13-support&quot;&gt;Laravel 13 support&lt;/h3&gt;
&lt;p&gt;The package now supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Laravel 10&lt;/li&gt;
&lt;li&gt;Laravel 11&lt;/li&gt;
&lt;li&gt;Laravel 12&lt;/li&gt;
&lt;li&gt;Laravel 13&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That should make it easier to use the same package across older and newer Laravel applications.&lt;/p&gt;
&lt;h3 id=&quot;import-diff-previews&quot;&gt;Import diff previews&lt;/h3&gt;
&lt;p&gt;You can now preview an import before writing anything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use App\Models\User;
use Olamilekan\GoogleSheets\Facades\GoogleSheets;

$preview = GoogleSheets::connection('users')
    -&amp;gt;diffAgainst(User::query(), key: 'email')
    -&amp;gt;rules([
        'name' =&amp;gt; ['required', 'string'],
        'email' =&amp;gt; ['required', 'email'],
    ])
    -&amp;gt;preview();

$preview-&amp;gt;counts();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The preview separates rows into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;new&lt;/li&gt;
&lt;li&gt;changed&lt;/li&gt;
&lt;li&gt;deleted&lt;/li&gt;
&lt;li&gt;invalid&lt;/li&gt;
&lt;li&gt;conflicts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;[
    'new' =&amp;gt; 1,
    'changed' =&amp;gt; 2,
    'deleted' =&amp;gt; 0,
    'invalid' =&amp;gt; 1,
    'conflicts' =&amp;gt; 0,
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also control which fields are compared:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$preview = GoogleSheets::connection('users')
    -&amp;gt;diffAgainst(User::query(), key: 'email')
    -&amp;gt;only(['name', 'role'])
    -&amp;gt;except(['updated_at'])
    -&amp;gt;preview();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful when building admin import screens or approval flows.&lt;/p&gt;
&lt;h3 id=&quot;sync-methods&quot;&gt;Sync methods&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;v1.1.0&lt;/code&gt; adds sync helpers for moving data between Google Sheets and common data sources.&lt;/p&gt;
&lt;p&gt;Sync from Eloquent to Google Sheets:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use App\Models\User;
use Olamilekan\GoogleSheets\Facades\GoogleSheets;

$report = GoogleSheets::connection('users')
    -&amp;gt;syncFromModel(User::class, keyColumn: 'email', options: [
        'columns' =&amp;gt; ['name', 'email', 'role'],
        'conflict' =&amp;gt; 'app_wins',
    ]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sync from Google Sheets to Eloquent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report = GoogleSheets::connection('users')
    -&amp;gt;syncToModel(User::class, keyColumn: 'email');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Import and export CSV files:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report = GoogleSheets::connection('users')
    -&amp;gt;importCsv(storage_path('app/users.csv'), keyColumn: 'email');

$report = GoogleSheets::connection('users')
    -&amp;gt;exportCsv(storage_path('app/users-export.csv'));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sync API data into a sheet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report = GoogleSheets::connection('orders')
    -&amp;gt;syncFromApi('https://api.example.com/orders', keyColumn: 'order_id', options: [
        'data_key' =&amp;gt; 'data',
        'headers' =&amp;gt; ['Authorization' =&amp;gt; 'Bearer '.$token],
    ]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push sheet data to an API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report = GoogleSheets::connection('orders')
    -&amp;gt;syncToApi('https://api.example.com/orders/bulk');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each sync returns a report:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report-&amp;gt;counts();
$report-&amp;gt;created();
$report-&amp;gt;updated();
$report-&amp;gt;conflicts();
$report-&amp;gt;errors();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;two-way-sync-with-conflict-handling&quot;&gt;Two-way sync with conflict handling&lt;/h3&gt;
&lt;p&gt;Two-way sync needs clear conflict behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$report = GoogleSheets::connection('users')
    -&amp;gt;syncTwoWay(User::class, keyColumn: 'email', options: [
        'conflict' =&amp;gt; 'fail',
    ]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Supported strategies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app_wins&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sheet_wins&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fail&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;dry-run-sync-command&quot;&gt;Dry-run sync command&lt;/h3&gt;
&lt;p&gt;You can preview command-line imports without writing rows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan google-sheets:sync &quot;App\\Imports\\UsersImport&quot; users --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dry runs compare rows using the import class target and key, then apply validation rules when available.&lt;/p&gt;
&lt;h3 id=&quot;validation-error-sheets&quot;&gt;Validation error sheets&lt;/h3&gt;
&lt;p&gt;Instead of only throwing validation errors, the package can write row-level issues into an error sheet.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$validRows = GoogleSheets::connection('users')-&amp;gt;validateWithErrorSheet([
    'name' =&amp;gt; ['required', 'string'],
    'email' =&amp;gt; ['required', 'email'],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives spreadsheet users a clearer place to fix bad rows.&lt;/p&gt;
&lt;h3 id=&quot;queue-sync-jobs&quot;&gt;Queue sync jobs&lt;/h3&gt;
&lt;p&gt;Longer syncs can be queued:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('users')
    -&amp;gt;queueSync('syncFromModel', [User::class, 'email'], queue: 'imports');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;audit-logs-and-notifications&quot;&gt;Audit logs and notifications&lt;/h3&gt;
&lt;p&gt;Sync activity is logged through Laravel's logger and kept in an in-process audit log:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$records = GoogleSheets::connection('users')-&amp;gt;syncAuditLog();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also send notifications:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('users')-&amp;gt;syncFromModel(User::class, 'email', [
    'notify' =&amp;gt; [
        'slack_webhook' =&amp;gt; config('services.slack.sync_webhook'),
        'mail_to' =&amp;gt; '*Emails are not allowed*',
    ],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;retry-and-backoff&quot;&gt;Retry and backoff&lt;/h3&gt;
&lt;p&gt;Temporary Google Sheets API failures are retried with exponential backoff and jitter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// config/google-sheets.php
'retry' =&amp;gt; [
    'enabled' =&amp;gt; true,
    'attempts' =&amp;gt; 3,
    'delay' =&amp;gt; 250,
    'max_delay' =&amp;gt; 5000,
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Runtime control:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$rows = GoogleSheets::withRetries(attempts: 5, delay: 500)-&amp;gt;all();
$rows = GoogleSheets::withoutRetries()-&amp;gt;all();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This helps with rate limits, quota throttling, backend errors, and other temporary failures.&lt;/p&gt;
&lt;h3 id=&quot;testing-fakes&quot;&gt;Testing fakes&lt;/h3&gt;
&lt;p&gt;You can test spreadsheet behavior without hitting the real API:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Olamilekan\GoogleSheets\Facades\GoogleSheets;

$fake = GoogleSheets::fake([
    'users' =&amp;gt; [
        ['name' =&amp;gt; 'Alice', 'email' =&amp;gt; '*Emails are not allowed*'],
    ],
]);

GoogleSheets::connection('users')-&amp;gt;appendAssoc([
    ['name' =&amp;gt; 'Bob', 'email' =&amp;gt; '*Emails are not allowed*'],
]);

$fake-&amp;gt;assertAppended('users', [
    'name' =&amp;gt; 'Bob',
    'email' =&amp;gt; '*Emails are not allowed*',
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;upgrade&quot;&gt;Upgrade&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer update olamilekan/laravel-google-sheets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you need the new config options:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan vendor:publish --tag=google-sheets-config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/oluwatosinolamilekan/laravel-google-sheets&quot;&gt;github.com/oluwatosinolamilekan/laravel-google-sheets&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Package:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require olamilekan/laravel-google-sheets
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Release:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer update olamilekan/laravel-google-sheets
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19417/building-a-production-ready-laravel-google-sheets-package-from-import-to-test-suite</guid>
<pubDate>Mon, 01 Jun 2026 11:25:30 +0000</pubDate>
</item>
<item>
<title>I Built a Full POS &amp; Inventory System with Laravel (Shop-Control)</title>
<link>https://coderlegion.com/19273/i-built-a-full-pos-inventory-system-with-laravel-shop-control</link>
<description>&lt;p&gt;A few days ago, I decided to go beyond tutorials and build something that reflects how real businesses actually operate.&lt;/p&gt;
&lt;p&gt;That’s how Shop-Control was born — a full-stack Point of Sale (POS) and inventory management system built with Laravel.&lt;/p&gt;
&lt;p&gt;This project helped me deeply understand how backend systems connect real business workflows like sales, inventory tracking, and customer management.&lt;/p&gt;
&lt;h3 id=&quot;-why-i-built-this-project&quot;&gt; Why I Built This Project&lt;/h3&gt;
&lt;p&gt;Most beginner Laravel projects stop at CRUD apps.&lt;/p&gt;
&lt;p&gt;I wanted to challenge myself by building something closer to a real production system — something that handles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sales processing&lt;/li&gt;
&lt;li&gt;Inventory updates in real-time&lt;/li&gt;
&lt;li&gt;Role-based access control&lt;/li&gt;
&lt;li&gt;Business reporting logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of isolated features, I focused on how everything connects together as a system.&lt;/p&gt;
&lt;h3 id=&quot;-what-shop-control-does&quot;&gt; What Shop-Control Does&lt;/h3&gt;
&lt;p&gt;Shop-Control simulates a real retail environment where different users interact with the system:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;1. Admins &amp;amp; Staff&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manage users and roles&lt;/li&gt;
&lt;li&gt;Monitor business performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt; 2. Products &amp;amp; Inventory&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add, update, and track stock&lt;/li&gt;
&lt;li&gt;Organize products by categories&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt; 3. Point of Sale (POS)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Add items to cart&lt;/li&gt;
&lt;li&gt;Calculate totals dynamically&lt;/li&gt;
&lt;li&gt;Process sales transactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Customers&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store customer data&lt;/li&gt;
&lt;li&gt;Track purchase history&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. Analytics Dashboard&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;View sales summaries&lt;/li&gt;
&lt;li&gt;Track inventory changes&lt;/li&gt;
&lt;li&gt;Monitor business activity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-tech-stack&quot;&gt;⚙️ Tech Stack&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Laravel – Backend framework and business logic&lt;/li&gt;
&lt;li&gt;Blade Templates – Server-side rendering&lt;/li&gt;
&lt;li&gt;SQLite – Lightweight database for local development&lt;/li&gt;
&lt;li&gt;Tailwind CSS / Bootstrap – UI styling&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-what-i-learned-building-this&quot;&gt; What I Learned Building This&lt;/h3&gt;
&lt;p&gt;This project taught me more than any tutorial ever did:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Database Design Matters More Than Code&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Poor relationships = broken system logic.&lt;/p&gt;
&lt;p&gt;I had to carefully design relationships between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;products&lt;/li&gt;
&lt;li&gt;sales&lt;/li&gt;
&lt;li&gt;customers&lt;/li&gt;
&lt;li&gt;cart sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;strong&gt;2. Business Logic Lives in the Backend&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A POS system is not just UI — it’s rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stock must update after every sale&lt;/li&gt;
&lt;li&gt;carts must calculate totals correctly&lt;/li&gt;
&lt;li&gt;users must only access allowed features&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;strong&gt;3. Real Systems Are About Flow, Not Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The hardest part wasn’t building features — it was making them work together smoothly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Preview&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Dashboard Overview: &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5833203801206867834&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. POS / Checkout Screen&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4094582504868460955&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Product &amp;amp; Inventory Management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4340545494802090295&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Sales History / Transactions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=589222154116149754&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; What’s Next&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I plan to improve Shop-Control by adding:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;REST API version for mobile apps&lt;/li&gt;
&lt;li&gt;PDF invoice generation&lt;/li&gt;
&lt;li&gt;Advanced analytics dashboard&lt;/li&gt;
&lt;li&gt;Barcode scanning support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt; Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building Shop-Control changed how I see Laravel.&lt;/p&gt;
&lt;p&gt;It stopped being just a framework for small apps — and became a tool for building real business systems.&lt;/p&gt;
&lt;p&gt;If you're learning Laravel, my advice is simple:&lt;/p&gt;
&lt;p&gt; Stop only building tutorials. Start building systems.&lt;/p&gt;
&lt;p&gt;That’s where real growth happens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; I’d love to hear from other developers:&lt;/strong&gt;&lt;br&gt;
What was the first project that made Laravel “click” for you?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19273/i-built-a-full-pos-inventory-system-with-laravel-shop-control</guid>
<pubDate>Sat, 30 May 2026 07:27:21 +0000</pubDate>
</item>
<item>
<title> 3 Things Building MediTrack Taught Me About Laravel (and Backend Development)</title>
<link>https://coderlegion.com/19141/3-things-building-meditrack-taught-me-about-laravel-and-backend-development</link>
<description>&lt;p&gt;A few days ago, I built MediTrack, a Laravel-based hospital management system for managing doctors, patients, appointments, prescriptions, and lab tests.&lt;/p&gt;
&lt;p&gt;This was one of the first projects where I moved beyond basic CRUD applications and started thinking in terms of real system design and relationships between features.&lt;/p&gt;
&lt;p&gt;While building it, I realized something important:&lt;/p&gt;
&lt;p&gt;Laravel starts making real sense when you stop following tutorials and start building complete systems.&lt;/p&gt;
&lt;p&gt;MediTrack forced me to think about structure, data flow, and how different parts of a system actually connect together in production-style applications.&lt;/p&gt;
&lt;p&gt;Here are 3 key lessons I learned:&lt;/p&gt;
&lt;p&gt;1️⃣ &lt;strong&gt;Database Design Shapes Everything&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At the beginning, I underestimated how important database design really is.&lt;/p&gt;
&lt;p&gt;I had entities like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;doctors → appointments&lt;/li&gt;
&lt;li&gt;patients → prescriptions&lt;/li&gt;
&lt;li&gt;lab tests → patients&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But as the project grew, I realized something critical:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;If your database structure is wrong, everything built on top of it becomes harder to maintain.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I had to go back multiple times to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;restructure tables&lt;/li&gt;
&lt;li&gt;adjust relationships&lt;/li&gt;
&lt;li&gt;rethink migrations&lt;/li&gt;
&lt;li&gt;fix design decisions that didn’t scale well&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where Laravel relationships like hasMany and belongsTo started to actually make sense in a practical way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Key takeaway:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;backend-development-is-mostly-about-designing-data-structures-correctly-before-writing-logic-&quot;&gt;Backend development is mostly about designing data structures correctly before writing logic.&lt;/h3&gt;
&lt;p&gt;2️⃣ &lt;strong&gt;Authentication Is About System Security, Not UI&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before this project, I thought authentication meant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;login&lt;/li&gt;
&lt;li&gt;register&lt;/li&gt;
&lt;li&gt;logout&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But MediTrack introduced role-based access like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Admin&lt;/li&gt;
&lt;li&gt;Doctor&lt;/li&gt;
&lt;li&gt;Receptionist&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that changed everything.&lt;/p&gt;
&lt;p&gt;Each role needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;different dashboards&lt;/li&gt;
&lt;li&gt;different permissions&lt;/li&gt;
&lt;li&gt;restricted routes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;protected backend logic&lt;/p&gt;
&lt;p&gt;This is where I learned about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Laravel middleware&lt;/li&gt;
&lt;li&gt;route protection&lt;/li&gt;
&lt;li&gt;backend authorization logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One important realization:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Hiding UI elements is not security. Backend rules are what actually protect the system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;authentication-is-not-a-feature-it-s-a-system-design-decision-&quot;&gt;Authentication is not a feature — it’s a system design decision.&lt;/h3&gt;
&lt;p&gt;3️⃣ &lt;strong&gt;Real Projects Teach What Tutorials Don’t&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tutorials helped me understand Laravel basics.&lt;/p&gt;
&lt;p&gt;But MediTrack is where things became real.&lt;/p&gt;
&lt;p&gt;I faced issues like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;features breaking after small changes&lt;/li&gt;
&lt;li&gt;incorrect database query results&lt;/li&gt;
&lt;li&gt;debugging unexpected behavior&lt;/li&gt;
&lt;li&gt;spending hours fixing small logic mistakes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike tutorials, there was no step-by-step solution.&lt;/p&gt;
&lt;p&gt;I had to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;read documentation&lt;/li&gt;
&lt;li&gt;debug systematically&lt;/li&gt;
&lt;li&gt;test multiple approaches&lt;/li&gt;
&lt;li&gt;learn through failure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And that’s where real growth happened.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key takeaway:&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;you-don-t-learn-development-by-watching-you-learn-it-by-building-breaking-and-fixing-&quot;&gt;You don’t learn development by watching — you learn it by building, breaking, and fixing.&lt;/h3&gt;
&lt;p&gt; &lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MediTrack is still evolving, and there’s a lot I want to improve.&lt;/p&gt;
&lt;p&gt;But it changed how I approach development.&lt;/p&gt;
&lt;p&gt;Laravel feels much clearer now because I’ve used it to build a complete system, not just small examples.&lt;/p&gt;
&lt;p&gt;If you’re learning backend development, my advice is simple:&lt;/p&gt;
&lt;p&gt;Start building real projects early — even if they’re imperfect.&lt;/p&gt;
&lt;p&gt;The learning that comes from debugging your own system is completely different from following tutorials.&lt;/p&gt;
&lt;p&gt;That’s where real developer thinking starts.&lt;/p&gt;
&lt;p&gt; I’d love to hear from others:&lt;br&gt;
&lt;strong&gt;What project taught you the most about development?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt; Open to feedback, collaboration, and backend engineering opportunities.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19141/3-things-building-meditrack-taught-me-about-laravel-and-backend-development</guid>
<pubDate>Thu, 28 May 2026 09:25:53 +0000</pubDate>
</item>
<item>
<title>How I Built a Hospital Management System with Laravel</title>
<link>https://coderlegion.com/18881/how-i-built-a-hospital-management-system-with-laravel</link>
<description>&lt;p&gt;Hey everyone! I'm Ibtisam, a developer from Somaliland learning full-stack web development. Recently, I built MediTrack, a hospital management system with Laravel, and I want to share what I learned along the way.&lt;/p&gt;
&lt;h3 id=&quot;why-i-built-meditrack&quot;&gt;Why I Built MediTrack&lt;/h3&gt;
&lt;p&gt;I wanted to understand how real-world applications work beyond simple CRUD projects. Healthcare systems seemed like a perfect challenge because they involve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple user roles (Admin, Doctor, Receptionist)&lt;/li&gt;
&lt;li&gt;Complex data relationships (Doctors → Patients → Appointments)&lt;/li&gt;
&lt;li&gt;Real business logic (managing prescriptions, lab tests)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I built MediTrack to learn and create a solid portfolio project.&lt;/p&gt;
&lt;h3 id=&quot;what-meditrack-does&quot;&gt;What MediTrack Does&lt;/h3&gt;
&lt;p&gt;It's a role-based hospital management system that helps clinics manage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Doctors&lt;/li&gt;
&lt;li&gt;Patients&lt;/li&gt;
&lt;li&gt;Appointments&lt;/li&gt;
&lt;li&gt;Prescriptions&lt;/li&gt;
&lt;li&gt;Lab Tests&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;features-&quot;&gt;Features:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Secure authentication (login/register)&lt;/li&gt;
&lt;li&gt;Real-time dashboard with statistics&lt;/li&gt;
&lt;li&gt;Full CRUD operations for all modules&lt;/li&gt;
&lt;li&gt;Search functionality&lt;/li&gt;
&lt;li&gt;Responsive Bootstrap UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-tech-stack-and-why-&quot;&gt;The Tech Stack (And Why)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Laravel&lt;/strong&gt; – Powerful yet beginner-friendly framework with great routing and authentication&lt;br&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; – Handles complex database relationships easily&lt;br&gt;
&lt;strong&gt;Bootstrap 5&lt;/strong&gt; – Build a clean UI without spending time on CSS&lt;br&gt;
&lt;strong&gt;Blade Templates&lt;/strong&gt; – Dynamic templating that feels natural in PHP&lt;/p&gt;
&lt;h3 id=&quot;the-3-biggest-challenges-and-how-i-solved-them-&quot;&gt;The 3 Biggest Challenges (And How I Solved Them)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Database Design&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Problem: Connecting Doctors, Patients, Appointments, and Prescriptions without breaking things.&lt;/p&gt;
&lt;p&gt;Solution: I learned about Laravel relationships (hasMany, belongsTo) and spent time planning my schema before coding. This saved me from major refactoring later.&lt;/p&gt;
&lt;p&gt;Lesson: Plan your database first. It's harder to change later.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Authentication &amp;amp; Authorization&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Problem: Different users (Admin, Doctor, Receptionist) need different dashboards and permissions.&lt;/p&gt;
&lt;p&gt;Solution: Used Laravel's built-in middleware and route guards to check user roles before showing data.&lt;/p&gt;
&lt;p&gt;Lesson: Never trust the frontend for security. Always validate on the backend.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Getting Real-Time Statistics&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Problem: The dashboard needed to show live counts of doctors, patients, appointments, etc.&lt;/p&gt;
&lt;p&gt;Solution: Used Laravel's query builders and Eloquent to aggregate data efficiently, then displayed it with simple JavaScript.&lt;/p&gt;
&lt;p&gt;Lesson: Database queries matter. Bad queries slow down your app fast.&lt;/p&gt;
&lt;h3 id=&quot;here-s-what-it-looks-like&quot;&gt;Here's What It Looks Like&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15611261109797804361&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Managing Doctors&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15142344309849620613&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Patients Overview&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8099417863890384089&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Appointments Management&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10356771585695753484&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Labs Management&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=875916281168707232&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Medication Management&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10098591982593995947&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;key-lessons-for-beginners&quot;&gt;Key Lessons for Beginners&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with a real problem&lt;/strong&gt; – Don't build random projects. Build something that solves a real need.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan before coding&lt;/strong&gt; – Spend 30 minutes sketching your database. It saves hours later.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the framework's tools&lt;/strong&gt; – Laravel has authentication, routing, and validation built-in. Use them instead of reinventing the wheel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test locally first&lt;/strong&gt; – Make sure features work before pushing to production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document your code&lt;/strong&gt; – Future you (and contributors) will thank you.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;check-it-out&quot;&gt;Check It Out&lt;/h3&gt;
&lt;p&gt;Want to see the full code, contribute, or just browse around?&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/adventureibtisam111/meditrack&quot;&gt;github.com/adventureibtisam111/meditrack&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If MediTrack helped you learn something or you find it useful, I'd love your support:&lt;/p&gt;
&lt;p&gt;⭐ Star the repo – Shows other developers it's helpful&lt;/p&gt;
&lt;p&gt; Drop feedback – Issues and suggestions are welcome&lt;/p&gt;
&lt;p&gt; [Sponsor my work] (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sponsors/adventureibtisam111&quot;&gt;https://github.com/sponsors/adventureibtisam111&lt;/a&gt;)  – Helps me build more educational projects like this&lt;/p&gt;
&lt;p&gt;That's it! Building MediTrack taught me that full-stack development isn't magic—it's just solving problems step by step.&lt;/p&gt;
&lt;p&gt;If you're learning Laravel or building your first full-stack project, I hope MediTrack inspires you. Let me know if you have questions or want to collaborate!&lt;/p&gt;
&lt;p&gt;Happy coding! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18881/how-i-built-a-hospital-management-system-with-laravel</guid>
<pubDate>Mon, 25 May 2026 12:07:28 +0000</pubDate>
</item>
<item>
<title>Building A Laravel Google Sheets Package That Imports, Exports, Caches, Formats, And Tests Cleanly</title>
<link>https://coderlegion.com/18467/building-laravel-google-sheets-package-that-imports-exports-caches-formats-tests-cleanly</link>
<description>&lt;p&gt;Google Sheets often starts as a quick operational tool. A support team tracks users in a sheet, finance exports monthly reports, or an internal dashboard needs a simple spreadsheet backend. The challenge is keeping that workflow Laravel-friendly once it grows beyond a few API calls.&lt;/p&gt;
&lt;p&gt;This package wraps the Google Sheets API with a fluent Laravel API for common application tasks: importing rows, exporting reports, managing multiple spreadsheet connections, caching reads, formatting tabs, and testing without hitting Google.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require olamilekan/laravel-google-sheets
php artisan vendor:publish --tag=google-sheets-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add your service account credentials path:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dotenv&quot;&gt;GOOGLE_SHEETS_CREDENTIALS_PATH=/path/to/service-account.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then configure named spreadsheet connections:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;'sheets' =&amp;gt; [
    'users' =&amp;gt; [
        'spreadsheet_id' =&amp;gt; env('GOOGLE_SHEETS_USERS_SPREADSHEET_ID'),
        'sheet' =&amp;gt; 'Users',
    ],

    'reports' =&amp;gt; [
        'spreadsheet_id' =&amp;gt; env('GOOGLE_SHEETS_REPORTS_SPREADSHEET_ID'),
        'sheet' =&amp;gt; 'Monthly',
    ],
],
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;import-users-from-google-sheets&quot;&gt;Import Users From Google Sheets&lt;/h3&gt;
&lt;p&gt;For a simple import, read rows from a named connection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$rows = GoogleSheets::connection('users')-&amp;gt;all();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a reusable import, create an import class:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use App\Models\User;
use Olamilekan\GoogleSheets\Imports\SheetImport;

class UsersImport extends SheetImport
{
    public function rules(): array
    {
        return ['email' =&amp;gt; ['required', 'email']];
    }

    public function model(array $row): User
    {
        return User::updateOrCreate(
            ['email' =&amp;gt; $row['email']],
            ['name' =&amp;gt; $row['name']]
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it from code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::import(new UsersImport(), 'users');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or from Artisan:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan google-sheets:sync &quot;App\\Imports\\UsersImport&quot; users
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;export-reports-to-google-sheets&quot;&gt;Export Reports To Google Sheets&lt;/h3&gt;
&lt;p&gt;Export classes keep reporting logic out of controllers and commands:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use App\Models\Report;
use Olamilekan\GoogleSheets\Exports\SheetExport;

class ReportsExport extends SheetExport
{
    public bool $replace = true;

    public function headings(): array
    {
        return ['Date', 'Name', 'Total'];
    }

    public function collection()
    {
        return Report::query()
            -&amp;gt;latest()
            -&amp;gt;get()
            -&amp;gt;map(fn (Report $report) =&amp;gt; [
                $report-&amp;gt;created_at-&amp;gt;toDateString(),
                $report-&amp;gt;name,
                $report-&amp;gt;total,
            ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then export:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::export(new ReportsExport(), 'reports');
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;header-aware-appends-and-upserts&quot;&gt;Header-Aware Appends And Upserts&lt;/h3&gt;
&lt;p&gt;Sheets usually have headers. Instead of manually ordering every cell, append associative arrays:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('users')-&amp;gt;appendAssoc([
    ['name' =&amp;gt; 'Alice', 'email' =&amp;gt; '*Emails are not allowed*', 'role' =&amp;gt; 'admin'],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Upsert rows by a key column:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('users')-&amp;gt;upsert('email', [
    ['name' =&amp;gt; 'Alice Updated', 'email' =&amp;gt; '*Emails are not allowed*', 'role' =&amp;gt; 'owner'],
    ['name' =&amp;gt; 'Bob', 'email' =&amp;gt; '*Emails are not allowed*', 'role' =&amp;gt; 'user'],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;validation-and-required-headers&quot;&gt;Validation And Required Headers&lt;/h3&gt;
&lt;p&gt;Catch bad spreadsheet data before it reaches your app:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('users')-&amp;gt;requireHeaders(['name', 'email', 'role']);

$rows = GoogleSheets::connection('users')-&amp;gt;validate([
    'name' =&amp;gt; ['required', 'string'],
    'email' =&amp;gt; ['required', 'email'],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;multi-connection-workflows&quot;&gt;Multi-Connection Workflows&lt;/h3&gt;
&lt;p&gt;Named connections make it easy to separate workflows:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$users = GoogleSheets::connection('users')-&amp;gt;all();

GoogleSheets::connection('reports')-&amp;gt;append([
    ['2026-05-17', 'Monthly Revenue', 15000],
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;caching&quot;&gt;Caching&lt;/h3&gt;
&lt;p&gt;Enable caching with Laravel's cache system:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dotenv&quot;&gt;GOOGLE_SHEETS_CACHE_ENABLED=true
GOOGLE_SHEETS_CACHE_STORE=redis
GOOGLE_SHEETS_CACHE_TTL=600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or turn it on per call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$rows = GoogleSheets::connection('users')-&amp;gt;enableCache(300)-&amp;gt;all();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When writes happen, the package clears remembered read cache keys for that spreadsheet so later reads can refresh.&lt;/p&gt;
&lt;h3 id=&quot;formatting-formulas-and-named-ranges&quot;&gt;Formatting, Formulas, And Named Ranges&lt;/h3&gt;
&lt;p&gt;Reports often need more than raw data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;GoogleSheets::connection('reports')
    -&amp;gt;sheet('Monthly')
    -&amp;gt;boldHeader()
    -&amp;gt;freezeRows()
    -&amp;gt;autoResizeColumns(1, 4);

GoogleSheets::connection('reports')-&amp;gt;append([
    ['Total', GoogleSheets::formula('SUM(C2:C100)')],
]);

$summary = GoogleSheets::connection('reports')
    -&amp;gt;namedRange('MonthlySummary')
    -&amp;gt;get();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;testing&quot;&gt;Testing&lt;/h3&gt;
&lt;p&gt;You can fake Google Sheets in tests:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$fake = GoogleSheets::fake([
    'users' =&amp;gt; [
        ['name' =&amp;gt; 'Alice', 'email' =&amp;gt; '*Emails are not allowed*'],
    ],
]);

GoogleSheets::connection('users')-&amp;gt;appendAssoc([
    ['name' =&amp;gt; 'Bob', 'email' =&amp;gt; '*Emails are not allowed*'],
]);

$fake-&amp;gt;assertAppended('users', ['name' =&amp;gt; 'Bob', 'email' =&amp;gt; '*Emails are not allowed*']);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;useful-commands&quot;&gt;Useful Commands&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan google-sheets:list users
php artisan google-sheets:clear reports --sheet=Monthly --range=A2:D100
php artisan google-sheets:sync &quot;App\\Exports\\ReportsExport&quot; reports
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;The goal is to make Google Sheets feel like a natural Laravel integration: fluent for simple reads and writes, structured for import and export classes, cache-aware for production use, and fakeable in tests.&lt;/p&gt;
&lt;p&gt;Read more on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/oluwatosinolamilekan/laravel-google-sheets&quot;&gt;github.com/oluwatosinolamilekan/laravel-google-sheets&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18467/building-laravel-google-sheets-package-that-imports-exports-caches-formats-tests-cleanly</guid>
<pubDate>Wed, 20 May 2026 15:09:00 +0000</pubDate>
</item>
<item>
<title>Multi-Tenant Security in SaaS: Data Isolation Patterns That Actually Work</title>
<link>https://coderlegion.com/16625/multi-tenant-security-in-saas-data-isolation-patterns-that-actually-work</link>
<description>&lt;hr&gt;
&lt;p&gt;Multi-tenancy is the economic engine of SaaS. Sharing infrastructure across customers reduces cost and simplifies operations. But it introduces a risk that can end your business overnight: tenant data leakage.&lt;/p&gt;
&lt;p&gt;When one customer can see another customer's data — even accidentally — the consequences are severe. Regulatory fines, contract termination, public disclosure requirements, and irreparable trust damage. I have worked on multi-tenant platforms serving hundreds of organisations, and I have learned that data isolation is not something you bolt on later. It is a foundational architectural decision that shapes everything from database design to query patterns to testing strategy.&lt;/p&gt;
&lt;p&gt;This article covers the three main tenancy models, practical implementation patterns in Laravel and Node.js, and the security controls that prevent data leakage in production.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-three-tenancy-models&quot;&gt;The Three Tenancy Models&lt;/h3&gt;
&lt;h4 id=&quot;1-database-per-tenant&quot;&gt;1. Database-per-Tenant&lt;/h4&gt;
&lt;p&gt;Each tenant gets a completely separate database. Maximum isolation, maximum operational complexity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Strongest isolation. Easy compliance with data residency requirements. Independent backup and restore per tenant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Connection management overhead. Schema migrations must run across all databases. Does not scale well beyond a few hundred tenants.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Regulated industries (healthcare, finance), enterprise customers with strict data sovereignty requirements.&lt;/p&gt;
&lt;h4 id=&quot;2-schema-per-tenant&quot;&gt;2. Schema-per-Tenant&lt;/h4&gt;
&lt;p&gt;All tenants share a database server, but each gets a separate schema (or namespace). Moderate isolation with lower overhead than separate databases.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Good isolation without connection management complexity. PostgreSQL handles this natively with schemas.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Not supported equally across all databases. MySQL schema separation is effectively database separation. Migrations still need to run per schema.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Mid-market SaaS with tens to low hundreds of tenants.&lt;/p&gt;
&lt;h4 id=&quot;3-shared-database-with-row-level-isolation&quot;&gt;3. Shared Database with Row-Level Isolation&lt;/h4&gt;
&lt;p&gt;All tenants share the same tables. Every row includes a &lt;code&gt;tenant_id&lt;/code&gt; column. Isolation is enforced at the application and query level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Simplest to operate. Migrations run once. Scales to thousands of tenants. Lowest infrastructure cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; One missed WHERE clause leaks data. Requires rigorous enforcement at every layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; High-volume SaaS, platforms with many small tenants, startups optimising for speed.&lt;/p&gt;
&lt;p&gt;For most SaaS applications, &lt;strong&gt;shared database with row-level isolation&lt;/strong&gt; is the right starting point. It is also the hardest to get right, because isolation depends entirely on your code never forgetting to filter by tenant. The rest of this article focuses on making that bulletproof.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tenant-context-management&quot;&gt;Tenant Context Management&lt;/h3&gt;
&lt;p&gt;The foundation of row-level tenancy is a reliable tenant context — a mechanism that ensures every database query automatically includes the correct tenant filter.&lt;/p&gt;
&lt;h4 id=&quot;laravel-global-scopes-and-middleware&quot;&gt;Laravel: Global Scopes and Middleware&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Http\Middleware;

use App\Services\TenantContext;
use Closure;
use Illuminate\Http\Request;

class ResolveTenant
{
    public function __construct(private TenantContext $tenantContext) {}

    public function handle(Request $request, Closure $next)
    {
        $tenantId = $this-&amp;gt;resolveTenantId($request);

        if (!$tenantId) {
            return response()-&amp;gt;json(['error' =&amp;gt; 'Tenant could not be resolved.'], 403);
        }

        $this-&amp;gt;tenantContext-&amp;gt;set($tenantId);

        $response = $next($request);

        $this-&amp;gt;tenantContext-&amp;gt;clear();

        return $response;
    }

    private function resolveTenantId(Request $request): ?string
    {
        if ($request-&amp;gt;hasHeader('X-Tenant-ID')) {
            return $this-&amp;gt;validateTenantAccess(
                $request-&amp;gt;user(),
                $request-&amp;gt;header('X-Tenant-ID')
            );
        }

        $host = $request-&amp;gt;getHost();
        $subdomain = explode('.', $host)[0] ?? null;

        if ($subdomain) {
            return $this-&amp;gt;resolveTenantBySubdomain($subdomain);
        }

        return $request-&amp;gt;user()?-&amp;gt;default_tenant_id;
    }

    private function validateTenantAccess($user, string $tenantId): ?string
    {
        if (!$user) return null;

        $hasAccess = $user-&amp;gt;tenants()-&amp;gt;where('tenant_id', $tenantId)-&amp;gt;exists();
        return $hasAccess ? $tenantId : null;
    }

    private function resolveTenantBySubdomain(string $subdomain): ?string
    {
        return cache()-&amp;gt;remember(
            &quot;tenant:subdomain:{$subdomain}&quot;,
            now()-&amp;gt;addHours(1),
            fn () =&amp;gt; \App\Models\Tenant::where('subdomain', $subdomain)-&amp;gt;value('id')
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Services;

class TenantContext
{
    private ?string $tenantId = null;

    public function set(string $tenantId): void
    {
        $this-&amp;gt;tenantId = $tenantId;
    }

    public function get(): string
    {
        if ($this-&amp;gt;tenantId === null) {
            throw new \RuntimeException('Tenant context not set. This is a critical isolation failure.');
        }

        return $this-&amp;gt;tenantId;
    }

    public function clear(): void
    {
        $this-&amp;gt;tenantId = null;
    }

    public function isSet(): bool
    {
        return $this-&amp;gt;tenantId !== null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;automatic-query-scoping-with-eloquent&quot;&gt;Automatic Query Scoping with Eloquent&lt;/h4&gt;
&lt;p&gt;The most dangerous aspect of row-level tenancy is forgetting to add &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; to a query. Eloquent global scopes eliminate this risk:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Models\Traits;

use App\Models\Scopes\TenantScope;
use App\Services\TenantContext;

trait BelongsToTenant
{
    protected static function bootBelongsToTenant(): void
    {
        static::addGlobalScope(new TenantScope());

        static::creating(function ($model) {
            if (!$model-&amp;gt;tenant_id) {
                $model-&amp;gt;tenant_id = app(TenantContext::class)-&amp;gt;get();
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Models\Scopes;

use App\Services\TenantContext;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $tenantContext = app(TenantContext::class);

        if ($tenantContext-&amp;gt;isSet()) {
            $builder-&amp;gt;where($model-&amp;gt;getTable() . '.tenant_id', $tenantContext-&amp;gt;get());
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now every model that uses the &lt;code&gt;BelongsToTenant&lt;/code&gt; trait automatically filters by tenant:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Invoice extends Model
{
    use BelongsToTenant;

    protected $fillable = ['tenant_id', 'number', 'amount', 'status'];
}

// These queries automatically include WHERE tenant_id = ?
$invoices = Invoice::where('status', 'pending')-&amp;gt;get();
$invoice = Invoice::findOrFail($id);
$count = Invoice::count();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;node-js-middleware-and-query-builder-integration&quot;&gt;Node.js: Middleware and Query Builder Integration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Request, Response, NextFunction } from 'express';
import { AsyncLocalStorage } from 'async_hooks';

interface TenantContextData {
  tenantId: string;
}

export const tenantStorage = new AsyncLocalStorage&amp;lt;TenantContextData&amp;gt;();

export function getTenantId(): string {
  const context = tenantStorage.getStore();
  if (!context?.tenantId) {
    throw new Error('Tenant context not set. This is a critical isolation failure.');
  }
  return context.tenantId;
}

export function tenantMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
): void {
  const tenantId = resolveTenantId(req);

  if (!tenantId) {
    res.status(403).json({ error: 'Tenant could not be resolved.' });
    return;
  }

  tenantStorage.run({ tenantId }, () =&amp;gt; next());
}

function resolveTenantId(req: Request): string | null {
  const headerTenant = req.headers['x-tenant-id'] as string;
  if (headerTenant) return headerTenant;

  const host = req.hostname;
  const subdomain = host.split('.')[0];
  if (subdomain) return subdomain;

  return (req as any).user?.defaultTenantId ?? null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;prisma-extension-for-automatic-tenant-filtering&quot;&gt;Prisma Extension for Automatic Tenant Filtering&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient, Prisma } from '@prisma/client';

function createTenantPrisma() {
  const prisma = new PrismaClient();

  return prisma.$extends({
    query: {
      $allOperations({ model, operation, args, query }) {
        const tenantModels = ['Invoice', 'Payment', 'Project', 'Document'];

        if (!model || !tenantModels.includes(model)) {
          return query(args);
        }

        const tenantId = getTenantId();

        if (['findMany', 'findFirst', 'findUnique', 'count', 'aggregate'].includes(operation)) {
          args.where = { ...args.where, tenantId };
        }

        if (['create', 'createMany'].includes(operation)) {
          if (operation === 'create') {
            args.data = { ...args.data, tenantId };
          }
        }

        if (['update', 'updateMany', 'delete', 'deleteMany'].includes(operation)) {
          args.where = { ...args.where, tenantId };
        }

        return query(args);
      },
    },
  });
}

export const prisma = createTenantPrisma();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;row-level-security-in-postgresql&quot;&gt;Row-Level Security in PostgreSQL&lt;/h3&gt;
&lt;p&gt;For an additional layer of defence, PostgreSQL's Row-Level Security (RLS) enforces tenant isolation at the database level — even if the application code has a bug:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set the tenant context at the beginning of each database connection:&lt;/p&gt;
&lt;h4 id=&quot;laravel-with-rls&quot;&gt;Laravel with RLS&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class SetTenantOnConnection
{
    public function handle(Request $request, Closure $next)
    {
        $tenantId = app(TenantContext::class)-&amp;gt;get();

        DB::statement(&quot;SET app.current_tenant_id = '{$tenantId}'&quot;);

        return $next($request);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-with-rls&quot;&gt;Node.js with RLS&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;async function withTenantRLS&amp;lt;T&amp;gt;(
  callback: (tx: PrismaClient) =&amp;gt; Promise&amp;lt;T&amp;gt;
): Promise&amp;lt;T&amp;gt; {
  const tenantId = getTenantId();

  return prisma.$transaction(async (tx) =&amp;gt; {
    await tx.$executeRawUnsafe(
      `SET LOCAL app.current_tenant_id = '${tenantId}'`
    );
    return callback(tx as any);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RLS acts as a safety net. Even if a developer writes a raw query that omits the tenant filter, PostgreSQL rejects rows that do not belong to the current tenant.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;testing-for-tenant-leakage&quot;&gt;Testing for Tenant Leakage&lt;/h3&gt;
&lt;p&gt;The most critical test you can write for a multi-tenant system verifies that Tenant A cannot access Tenant B's data:&lt;/p&gt;
&lt;h4 id=&quot;laravel-test&quot;&gt;Laravel Test&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class TenantIsolationTest extends TestCase
{
    public function test_tenant_cannot_access_other_tenant_data(): void
    {
        $tenantA = Tenant::factory()-&amp;gt;create();
        $tenantB = Tenant::factory()-&amp;gt;create();

        $invoiceA = Invoice::factory()-&amp;gt;create(['tenant_id' =&amp;gt; $tenantA-&amp;gt;id]);
        $invoiceB = Invoice::factory()-&amp;gt;create(['tenant_id' =&amp;gt; $tenantB-&amp;gt;id]);

        app(TenantContext::class)-&amp;gt;set($tenantA-&amp;gt;id);

        $visibleInvoices = Invoice::all();

        $this-&amp;gt;assertTrue($visibleInvoices-&amp;gt;contains($invoiceA));
        $this-&amp;gt;assertFalse($visibleInvoices-&amp;gt;contains($invoiceB));
    }

    public function test_tenant_cannot_access_other_tenant_by_id(): void
    {
        $tenantA = Tenant::factory()-&amp;gt;create();
        $tenantB = Tenant::factory()-&amp;gt;create();

        $invoiceB = Invoice::factory()-&amp;gt;create(['tenant_id' =&amp;gt; $tenantB-&amp;gt;id]);

        app(TenantContext::class)-&amp;gt;set($tenantA-&amp;gt;id);

        $this-&amp;gt;expectException(ModelNotFoundException::class);
        Invoice::findOrFail($invoiceB-&amp;gt;id);
    }

    public function test_queries_without_tenant_context_throw(): void
    {
        $this-&amp;gt;expectException(\RuntimeException::class);
        Invoice::all();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-test&quot;&gt;Node.js Test&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;describe('Tenant Isolation', () =&amp;gt; {
  it('should not return data from another tenant', async () =&amp;gt; {
    const tenantA = await createTenant();
    const tenantB = await createTenant();

    await createInvoice({ tenantId: tenantA.id, amount: 100 });
    await createInvoice({ tenantId: tenantB.id, amount: 200 });

    const invoices = await tenantStorage.run(
      { tenantId: tenantA.id },
      () =&amp;gt; prisma.invoice.findMany()
    );

    expect(invoices).toHaveLength(1);
    expect(invoices[0].tenantId).toBe(tenantA.id);
  });

  it('should throw when tenant context is missing', async () =&amp;gt; {
    await expect(prisma.invoice.findMany()).rejects.toThrow(
      'Tenant context not set'
    );
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run these tests on every deployment. A failing tenant isolation test should block the release.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-tenant-leakage-vectors&quot;&gt;Common Tenant Leakage Vectors&lt;/h3&gt;
&lt;p&gt;Even with global scopes and RLS, leakage can happen through:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Raw queries:&lt;/strong&gt; Any &lt;code&gt;DB::select()&lt;/code&gt; or &lt;code&gt;prisma.$queryRaw()&lt;/code&gt; bypasses ORM scoping. Audit every raw query for tenant filtering.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Background jobs:&lt;/strong&gt; Queue workers do not have HTTP request context. Tenant ID must be serialised into the job payload and restored before execution.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache keys:&lt;/strong&gt; If cache keys do not include the tenant ID, one tenant's cached data can be served to another.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File storage paths:&lt;/strong&gt; Uploaded files must be stored under tenant-specific prefixes. A flat storage structure risks cross-tenant file access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Admin endpoints:&lt;/strong&gt; Internal tools that query across tenants must be explicitly authorised and logged.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;laravel-tenant-aware-job-example&quot;&gt;Laravel: Tenant-Aware Job Example&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ProcessInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private string $tenantId,
        private int $invoiceId,
    ) {}

    public function handle(TenantContext $tenantContext): void
    {
        $tenantContext-&amp;gt;set($this-&amp;gt;tenantId);

        $invoice = Invoice::findOrFail($this-&amp;gt;invoiceId);
        // Process invoice within correct tenant context
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;production-results&quot;&gt;Production Results&lt;/h3&gt;
&lt;p&gt;After implementing comprehensive tenant isolation across multi-tenant platforms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Tenant data leakage incidents &lt;/td&gt;&lt;td&gt;2–3 per year &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Security audit findings (critical) &lt;/td&gt;&lt;td&gt;4 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Time to onboard new tenant &lt;/td&gt;&lt;td&gt;2 days &lt;/td&gt;&lt;td&gt;15 minutes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cross-tenant query bugs caught in CI &lt;/td&gt;&lt;td&gt;N/A &lt;/td&gt;&lt;td&gt;12 (prevented from reaching production)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Customer trust incidents &lt;/td&gt;&lt;td&gt;1 per year &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways-1&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with shared database + row-level isolation.&lt;/strong&gt; It scales to thousands of tenants and has the lowest operational overhead. Move to database-per-tenant only when regulation demands it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never rely on developers remembering to filter.&lt;/strong&gt; Use global scopes (Laravel) or Prisma extensions (Node.js) to make tenant filtering automatic and invisible.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add PostgreSQL RLS as a safety net.&lt;/strong&gt; Application-level filtering is necessary. Database-level filtering is insurance against application bugs.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test for leakage explicitly.&lt;/strong&gt; Write tests that create data in Tenant A, set context to Tenant B, and verify the data is invisible. Run these tests on every deployment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit raw queries, background jobs, cache keys, and file paths.&lt;/strong&gt; These are the most common vectors for accidental cross-tenant data exposure.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serialise tenant context into background jobs.&lt;/strong&gt; Queue workers do not inherit HTTP request context. The tenant ID must travel with the job.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Multi-tenant security is not a feature you add — it is a design constraint that shapes your entire architecture. The patterns in this article are not theoretical. They come from building platforms where a tenant leakage incident would mean regulatory consequences and broken customer relationships.&lt;/p&gt;
&lt;p&gt;Whether you build in Laravel or Node.js, the principles are the same: automatic query scoping, database-level enforcement, explicit testing, and paranoid attention to the edge cases where isolation breaks down. Get this right, and your multi-tenant platform can scale with confidence.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16625/multi-tenant-security-in-saas-data-isolation-patterns-that-actually-work</guid>
<pubDate>Mon, 04 May 2026 11:15:43 +0000</pubDate>
</item>
<item>
<title>Why Startups Are Choosing Laravel Over Other Frameworks in 2026</title>
<link>https://coderlegion.com/16325/why-startups-are-choosing-laravel-over-other-frameworks-in-2026</link>
<description>&lt;h2 id=&quot;quick-overview&quot;&gt;Quick Overview&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Startups in 2026 are choosing Laravel because it allows small teams&lt;br&gt;
to launch real products quickly.&lt;/li&gt;
&lt;li&gt;Its batteries-included ecosystem means less setup and more&lt;br&gt;
construction from day one.&lt;/li&gt;
&lt;li&gt;The developer experience is smooth, well-documented, and&lt;br&gt;
beginner-friendly.&lt;/li&gt;
&lt;li&gt;Infrastructure costs remain low; PHP hosting is affordable and widely&lt;br&gt;
available.&lt;/li&gt;
&lt;li&gt;Laravel's toolset is changing quickly to meet the needs of the AI&lt;br&gt;
era.&lt;/li&gt;
&lt;li&gt;In short, it eliminates the obstacles that slow down early-stage&lt;br&gt;
progress.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's the reality most startup founders and solo developers face today. A deadline that won't budge, a budget that has limits, and a framework decision that influences everything that follows. Choose poorly, and your team spends weeks struggling with setup instead of addressing real user problems. Choose well, and you'll live, making improvements and growing while others are still getting things ready. &lt;/p&gt;
&lt;p&gt;In 2026, more startups are coming to the same conclusion: Laravel. Not because it's trendy, but because it reliably produces results. It simplifies complex problems, automates repetitive tasks, and provides developers with the tools to work at the speed that early-stage companies need. And with the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.wpwebelite.com/blog/future-of-laravel/&quot;&gt;future of Laravel&lt;/a&gt; pushing deeper into AI-powered tooling, real-time features, and serverless infrastructure, the framework is only getting stronger from here. &lt;/p&gt;
&lt;h2 id=&quot;why-choose-laravel&quot;&gt;Why Choose Laravel&lt;/h2&gt;
&lt;p&gt;The short answer: Laravel removes friction. The longer answer includes everything from authentication to queues to deployment. Let's explore why this PHP framework has become the favorite for startups that need to move quickly without creating a technical mess they'll regret in six months. &lt;/p&gt;
&lt;h3 id=&quot;1-everything-you-need-ships-with-laravel-&quot;&gt;1. Everything You Need Ships With Laravel.&lt;/h3&gt;
&lt;p&gt;When you start a Laravel project, you are not piecing together parts from five different libraries. Authentication, database migrations, job queues, file storage, email sending, and API routing all come included. &lt;/p&gt;
&lt;p&gt;For a startup, this means your first week focuses on your product, not your infrastructure. Here’s how that works in practice: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eloquent ORM allows you to interact with your database using clean,&lt;br&gt;
readable PHP.&lt;/li&gt;
&lt;li&gt;Relationships, scopes, and eager loading are just a single method&lt;br&gt;
call away. You avoid raw SQL confusion.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://stackoverflow.com/questions/73289994/laravel-breeze-and-laravel-sanctum-how-to-safely-mix-authorizations&quot;&gt;Laravel Sanctum and Breeze&lt;/a&gt; provide token-based API authentication and&lt;br&gt;
full-stack starter kits right out of the box. Your login system takes&lt;br&gt;
hours instead of weeks.&lt;/li&gt;
&lt;li&gt;Queues and jobs let you offload slow tasks, like emails, image&lt;br&gt;
processing, and third-party API calls, to background workers with a&lt;br&gt;
single dispatch call.&lt;/li&gt;
&lt;li&gt;Testing is built in. PHPUnit and Pest are top-notch tools. You can&lt;br&gt;
write feature tests that access your routes and database without any&lt;br&gt;
configuration issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-the-developer-experience-is-genuinely-good&quot;&gt;2. The Developer Experience Is Genuinely Good&lt;/h3&gt;
&lt;p&gt;Frameworks succeed or fail based on how enjoyable they are to use. Laravel's documentation is among the best in any language ecosystem. It is clear, complete, and actively maintained. Tools like Laravel Herd for local development, Forge for server deployment, and Vapor for serverless on AWS mean your entire workflow, from local coding to cloud production, exists within a single cohesive ecosystem. This is important when you are a two-person team trying to ship quickly.&lt;/p&gt;
&lt;h3 id=&quot;3-it-s-cost-effective-for-lean-teams&quot;&gt;3. It's Cost-Effective for Lean Teams&lt;/h3&gt;
&lt;p&gt;PHP developers are widely available and generally more affordable than Node.js or Go specialists with the same level of experience. Shared and VPS hosting for Laravel apps is well-documented and inexpensive. And since Laravel handles so much out of the box, you need fewer developers to build the same product. For bootstrapped startups, that math matters a great deal.&lt;/p&gt;
&lt;h3 id=&quot;4-scalability-is-built-in-from-the-start-&quot;&gt;4. Scalability Is Built In From the Start.&lt;/h3&gt;
&lt;p&gt;Many people wrongly believe that Laravel is only for small apps. This is not true in 2026. Laravel Octane, powered by &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.reddit.com/r/laravel/comments/nbc742/laravel_octane_roadrunner_or_swoole_pros_and_cons/&quot;&gt;Swoole or RoadRunner&lt;/a&gt;, greatly increases throughput. Laravel Vapor manages serverless auto-scaling on AWS. Companies that process millions of requests daily use Laravel. You can start small and grow without needing to rewrite your stack.  &lt;/p&gt;
&lt;h3 id=&quot;5-laravel-is-already-built-for-the-ai-era-&quot;&gt;5. Laravel Is Already Built for the AI Era.&lt;/h3&gt;
&lt;p&gt;With Laravel Folio, Volt, and strong Livewire 3 integration, creating reactive UIs without a separate JavaScript framework is now easy. More importantly, the PHP AI ecosystem has quickly developed. Integrations with OpenAI, vector databases, and LLM pipelines are available as first-class packages.  &lt;/p&gt;
&lt;p&gt;The future of Laravel is clearly linked to the AI-augmented web, and the community is already building in that space. This isn't about catching up; it's about keeping up.&lt;/p&gt;
&lt;h3 id=&quot;6-community-and-hiring-are-a-real-advantage&quot;&gt;6. Community and Hiring Are a Real Advantage&lt;/h3&gt;
&lt;p&gt;Laracasts, Laravel's official learning platform, offers over 2,000 video lessons. The communities on Discord, Reddit, and GitHub are large and genuinely helpful. When you're a startup and face a problem at 11 pm, that support is more important than any metric.&lt;br&gt;
When it comes to hiring, there is a large pool of Laravel developers worldwide who can be productive from day one; no complicated onboarding is necessary.  &lt;/p&gt;
&lt;h3 id=&quot;7-one-more-reason-to-choose-laravel&quot;&gt;7. One More Reason to Choose Laravel&lt;/h3&gt;
&lt;p&gt;Beyond features and the ecosystem, there's a cultural fit. Laravel was created with developers in mind, not companies trying to standardize processes. That mindset is reflected in every API choice, every helper function, and every convention the framework establishes. It values your time. For a startup founder who is also the lead developer, that respect leads to delivered features and less burnout.  &lt;/p&gt;
&lt;h2 id=&quot;laravel-vs-the-alternatives-in-2026&quot;&gt;Laravel vs the Alternatives in 2026&lt;/h2&gt;
&lt;p&gt;You've probably heard the arguments for Django, Node/Express, or even Next.js as a full-stack choice. Here's how they honestly compare for most startup use cases:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17651500982757363740&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Startups in 2026 are not choosing Laravel because it's the only choice; they prefer it because it's the most straightforward option for their needs. It delivers quickly, scales effectively, costs less, and helps your team stay focused. Whether you're creating a SaaS product, a marketplace, or an API backend, Laravel provides the tools you need to go from idea to launch without unnecessary obstacles.&lt;br&gt;
If you're still uncertain about your stack, the evidence clearly points in one direction.&lt;/p&gt;
&lt;h2 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h2&gt;
&lt;h3 id=&quot;1-is-laravel-good-for-beginners-in-2026-&quot;&gt;1. Is Laravel good for beginners in 2026?&lt;/h3&gt;
&lt;p&gt;Yes, more than ever. Great documentation, thousands of lessons on Laracasts, and one-click local setup via Laravel Herd mean that most developers can have a working app up and running on their first day. The framework's conventions also guide beginners toward good habits from the start.  &lt;/p&gt;
&lt;h3 id=&quot;2-is-laravel-faster-than-node-js-&quot;&gt;2. Is Laravel faster than Node.js?&lt;/h3&gt;
&lt;p&gt;Node.js performs better on raw concurrency benchmarks, but with Laravel Octane enabled, the difference shrinks quickly. In most real-world apps, the bottleneck is database queries, not the framework. For the vast majority of startups, Laravel's performance is more than enough.  &lt;/p&gt;
&lt;h3 id=&quot;3-can-laravel-handle-large-scale-applications-&quot;&gt;3. Can Laravel handle large-scale applications?&lt;/h3&gt;
&lt;p&gt;Yes. Companies like Geocodio and Invoice Ninja serve millions of users using Laravel. With Redis caching, queue workers, horizontal scaling, and serverless deployment via Vapor, enterprise-level workloads are very achievable. The argument that &quot;Laravel doesn't scale&quot; is years out of date.  &lt;/p&gt;
&lt;h3 id=&quot;4-what-is-laravel-best-used-for-&quot;&gt;4. What is Laravel best used for?&lt;/h3&gt;
&lt;p&gt;SaaS products, REST APIs, e-commerce platforms, CMS systems, and marketplaces. Its built-in authentication, payments through Cashier, file storage, and queues make it ideal for products that need multiple features running together from day one.  &lt;/p&gt;
&lt;h3 id=&quot;5-is-laravel-still-relevant-in-2026-&quot;&gt;5. Is Laravel still relevant in 2026?&lt;/h3&gt;
&lt;p&gt;Absolutely. Annual major releases, support for AI integration, Livewire for reactive UIs, and performance gains with PHP 8.3+ keep Laravel up to date. Developer satisfaction scores remain high, and Laravel job postings continue to be strong globally, both of which are reliable signs of a healthy, growing ecosystem.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16325/why-startups-are-choosing-laravel-over-other-frameworks-in-2026</guid>
<pubDate>Fri, 01 May 2026 13:40:51 +0000</pubDate>
</item>
<item>
<title>Security by Design in Healthcare Data Platforms</title>
<link>https://coderlegion.com/16264/security-by-design-in-healthcare-data-platforms</link>
<description>&lt;p&gt;Healthcare data is among the most sensitive information a system can hold. Patient records, diagnostic results, treatment histories, and genomic data are not just private — they are regulated by laws that carry serious penalties for mishandling. HIPAA in the United States, GDPR in Europe, and Nigeria's NDPR all impose strict requirements on how healthcare data must be collected, stored, processed, and shared.&lt;/p&gt;
&lt;p&gt;Security in healthcare is not something you add after the product works. It must be woven into every layer of the system from the first line of code. I have built health-tech platforms that handle sensitive patient data collection and clinical workflows, and the lessons from that work apply broadly to any system where data protection is not optional.&lt;/p&gt;
&lt;p&gt;This article covers the architecture patterns, implementation strategies, and operational practices for building healthcare data platforms that are secure by design — with examples in both Laravel and Node.js.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-healthcare-systems-are-different&quot;&gt;Why Healthcare Systems Are Different&lt;/h3&gt;
&lt;p&gt;Most web applications can tolerate a brief security lapse and recover. Healthcare systems cannot. The consequences of a breach are uniquely severe:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Patient harm:&lt;/strong&gt; Leaked medical records can affect employment, insurance, and personal relationships. Unlike a leaked password, a disclosed medical condition cannot be reset.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regulatory penalties:&lt;/strong&gt; HIPAA violations can result in fines up to $1.5 million per violation category per year. GDPR fines can reach 4% of global annual revenue.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operational shutdown:&lt;/strong&gt; A breach in a healthcare system may require shutting down access to patient data while the investigation proceeds — directly affecting care delivery.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust destruction:&lt;/strong&gt; Patients who learn their data was compromised may refuse to use digital health tools, undermining the entire digital health ecosystem.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These consequences demand a security model that assumes breaches will be attempted and builds defences at every layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-security-architecture&quot;&gt;The Security Architecture&lt;/h3&gt;
&lt;p&gt;Healthcare data security operates on multiple layers:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Client Layer]
  - End-to-end encryption for data in transit
  - Session management with short TTLs
  - Input validation and sanitisation

[Application Layer]
  - Authentication (MFA required)
  - Role-Based Access Control (RBAC)
  - Audit logging of every data access
  - Field-level encryption for sensitive data

[Data Layer]
  - Encryption at rest (AES-256)
  - Database access controls
  - Backup encryption
  - Data retention and purging policies

[Infrastructure Layer]
  - Network segmentation
  - Intrusion detection
  - Vulnerability scanning
  - Access logging and monitoring
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;authentication-and-access-control&quot;&gt;Authentication and Access Control&lt;/h3&gt;
&lt;p&gt;Healthcare systems require strong authentication and granular access control. Not every user should see every patient's data, and every access must be logged.&lt;/p&gt;
&lt;h4 id=&quot;laravel-role-based-access-control&quot;&gt;Laravel: Role-Based Access Control&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Permission extends Model
{
    protected $fillable = ['name', 'resource', 'action', 'conditions'];
    protected $casts = ['conditions' =&amp;gt; 'array'];
}

class Role extends Model
{
    protected $fillable = ['name', 'description', 'level'];

    public function permissions()
    {
        return $this-&amp;gt;belongsToMany(Permission::class);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Cache;

class AccessControlService
{
    public function canAccess(User $user, string $resource, string $action, array $context = []): bool
    {
        $permissions = $this-&amp;gt;getUserPermissions($user);

        foreach ($permissions as $permission) {
            if ($permission-&amp;gt;resource !== $resource) continue;
            if ($permission-&amp;gt;action !== $action &amp;amp;&amp;amp; $permission-&amp;gt;action !== '*') continue;

            if ($permission-&amp;gt;conditions) {
                if (!$this-&amp;gt;evaluateConditions($permission-&amp;gt;conditions, $context, $user)) {
                    continue;
                }
            }

            $this-&amp;gt;logAccess($user, $resource, $action, $context, true);
            return true;
        }

        $this-&amp;gt;logAccess($user, $resource, $action, $context, false);
        return false;
    }

    private function evaluateConditions(array $conditions, array $context, User $user): bool
    {
        foreach ($conditions as $condition) {
            switch ($condition['type']) {
                case 'own_patients_only':
                    if (($context['patient_id'] ?? null) &amp;amp;&amp;amp;
                        !$user-&amp;gt;patients()-&amp;gt;where('id', $context['patient_id'])-&amp;gt;exists()) {
                        return false;
                    }
                    break;
                case 'department_match':
                    if (($context['department_id'] ?? null) !== $user-&amp;gt;department_id) {
                        return false;
                    }
                    break;
                case 'time_restricted':
                    $now = now();
                    if ($now-&amp;gt;hour &amp;lt; $condition['start_hour'] || $now-&amp;gt;hour &amp;gt; $condition['end_hour']) {
                        return false;
                    }
                    break;
            }
        }
        return true;
    }

    private function getUserPermissions(User $user)
    {
        return Cache::remember(
            &quot;user_permissions:{$user-&amp;gt;id}&quot;,
            now()-&amp;gt;addMinutes(15),
            fn () =&amp;gt; $user-&amp;gt;roles()-&amp;gt;with('permissions')-&amp;gt;get()-&amp;gt;pluck('permissions')-&amp;gt;flatten()
        );
    }

    private function logAccess(User $user, string $resource, string $action, array $context, bool $granted): void
    {
        \App\Models\AccessLog::create([
            'user_id' =&amp;gt; $user-&amp;gt;id,
            'resource' =&amp;gt; $resource,
            'action' =&amp;gt; $action,
            'context' =&amp;gt; $context,
            'granted' =&amp;gt; $granted,
            'ip_address' =&amp;gt; request()-&amp;gt;ip(),
            'user_agent' =&amp;gt; request()-&amp;gt;userAgent(),
            'accessed_at' =&amp;gt; now(),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-middleware-based-access-control&quot;&gt;Node.js: Middleware-Based Access Control&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Request, Response, NextFunction } from 'express';

export function requireAccess(resource: string, action: string) {
  return async (req: Request, res: Response, next: NextFunction) =&amp;gt; {
    const user = (req as any).user;
    if (!user) return res.status(401).json({ error: 'Authentication required' });

    const context = {
      patient_id: req.params.patientId,
      department_id: req.params.departmentId,
      ip_address: req.ip,
    };

    const granted = await checkAccess(user, resource, action, context);
    await logAccess({ userId: user.id, resource, action, context, granted,
      ipAddress: req.ip!, userAgent: req.headers['user-agent'] || '', accessedAt: new Date() });

    if (!granted) return res.status(403).json({ error: 'Access denied', resource, action });
    next();
  };
}

async function checkAccess(user: any, resource: string, action: string, context: Record&amp;lt;string, unknown&amp;gt;): Promise&amp;lt;boolean&amp;gt; {
  const permissions = await getUserPermissions(user.id);
  return permissions.some((perm: any) =&amp;gt; {
    if (perm.resource !== resource) return false;
    if (perm.action !== action &amp;amp;&amp;amp; perm.action !== '*') return false;
    if (perm.conditions) return perm.conditions.every((c: any) =&amp;gt; evaluateCondition(c, context, user));
    return true;
  });
}

function evaluateCondition(condition: any, context: Record&amp;lt;string, unknown&amp;gt;, user: any): boolean {
  switch (condition.type) {
    case 'own_patients_only': return user.patientIds?.includes(context.patient_id) ?? false;
    case 'department_match': return user.departmentId === context.department_id;
    default: return false;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply to routes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;router.get('/patients/:patientId/records', requireAccess('patient_records', 'read'), patientRecordController.list);
router.post('/patients/:patientId/records', requireAccess('patient_records', 'write'), patientRecordController.create);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;field-level-encryption&quot;&gt;Field-Level Encryption&lt;/h3&gt;
&lt;p&gt;Not all data in a patient record is equally sensitive. Field-level encryption encrypts specific fields within a record rather than the entire database.&lt;/p&gt;
&lt;h4 id=&quot;laravel-encrypted-attributes&quot;&gt;Laravel: Encrypted Attributes&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;trait EncryptsHealthData
{
    public function getAttribute($key)
    {
        $value = parent::getAttribute($key);
        if (in_array($key, static::$encryptedFields) &amp;amp;&amp;amp; $value !== null) {
            try { return Crypt::decryptString($value); } catch (\Exception) { return $value; }
        }
        return $value;
    }

    public function setAttribute($key, $value)
    {
        if (in_array($key, static::$encryptedFields) &amp;amp;&amp;amp; $value !== null) {
            $value = Crypt::encryptString($value);
        }
        return parent::setAttribute($key, $value);
    }
}

class PatientRecord extends Model
{
    use EncryptsHealthData;
    protected static array $encryptedFields = ['diagnosis', 'treatment_notes', 'medication_details', 'lab_results', 'genetic_data'];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-encryption-service&quot;&gt;Node.js: Encryption Service&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');

export function encryptField(plaintext: string): string {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);
  let encrypted = cipher.update(plaintext, 'utf8', 'hex') + cipher.final('hex');
  return `${iv.toString('hex')}:${cipher.getAuthTag().toString('hex')}:${encrypted}`;
}

export function decryptField(ciphertext: string): string {
  const [ivHex, authTagHex, encrypted] = ciphertext.split(':');
  const decipher = crypto.createDecipheriv(ALGORITHM, KEY, Buffer.from(ivHex, 'hex'));
  decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
  return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;comprehensive-audit-logging&quot;&gt;Comprehensive Audit Logging&lt;/h3&gt;
&lt;p&gt;Every access to patient data must be logged — this is a regulatory requirement. The audit log answers: who accessed what data, when, from where, and why.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class AuditPatientAccess
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);
        $patientId = $request-&amp;gt;route('patientId') ?? $request-&amp;gt;input('patient_id');

        if ($patientId) {
            AuditLog::create([
                'user_id' =&amp;gt; $request-&amp;gt;user()-&amp;gt;id,
                'user_role' =&amp;gt; $request-&amp;gt;user()-&amp;gt;primaryRole()-&amp;gt;name,
                'action' =&amp;gt; $request-&amp;gt;method() . ' ' . $request-&amp;gt;path(),
                'resource_type' =&amp;gt; 'patient_data',
                'resource_id' =&amp;gt; $patientId,
                'ip_address' =&amp;gt; $request-&amp;gt;ip(),
                'response_code' =&amp;gt; $response-&amp;gt;getStatusCode(),
                'request_params' =&amp;gt; $this-&amp;gt;sanitiseParams($request-&amp;gt;all()),
                'accessed_at' =&amp;gt; now(),
            ]);
        }
        return $response;
    }

    private function sanitiseParams(array $params): array
    {
        $sensitive = ['password', 'ssn', 'credit_card', 'diagnosis'];
        return collect($params)
            -&amp;gt;map(fn ($value, $key) =&amp;gt; in_array($key, $sensitive) ? '[REDACTED]' : $value)
            -&amp;gt;all();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;data-retention-and-purging&quot;&gt;Data Retention and Purging&lt;/h3&gt;
&lt;p&gt;Healthcare regulations specify minimum and maximum retention periods. Data must be available for the required period and securely destroyed afterward.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class DataRetentionService
{
    private const RETENTION_POLICIES = [
        'patient_records' =&amp;gt; ['min_years' =&amp;gt; 7, 'max_years' =&amp;gt; 10],
        'audit_logs'      =&amp;gt; ['min_years' =&amp;gt; 6, 'max_years' =&amp;gt; 7],
        'session_data'    =&amp;gt; ['min_years' =&amp;gt; 0, 'max_years' =&amp;gt; 1],
        'consent_records' =&amp;gt; ['min_years' =&amp;gt; 10, 'max_years' =&amp;gt; 15],
    ];

    public function enforceRetention(): array
    {
        $results = [];
        foreach (self::RETENTION_POLICIES as $dataType =&amp;gt; $policy) {
            $cutoff = now()-&amp;gt;subYears($policy['max_years']);
            $count = $this-&amp;gt;getModel($dataType)-&amp;gt;where('created_at', '&amp;lt;', $cutoff)-&amp;gt;count();
            if ($count &amp;gt; 0) {
                $this-&amp;gt;archiveAndPurge($dataType, $cutoff);
                $results[$dataType] = ['purged' =&amp;gt; $count, 'cutoff' =&amp;gt; $cutoff-&amp;gt;toDateString()];
            }
        }
        return $results;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;consent-management&quot;&gt;Consent Management&lt;/h3&gt;
&lt;p&gt;Patients must give informed consent for data collection and processing. Consent must be tracked, versioned, and revocable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ConsentService
{
    public function recordConsent(string $patientId, string $consentType, string $version, bool $granted, ?string $ipAddress = null): ConsentRecord
    {
        return ConsentRecord::create([
            'patient_id' =&amp;gt; $patientId,
            'consent_type' =&amp;gt; $consentType,
            'version' =&amp;gt; $version,
            'granted' =&amp;gt; $granted,
            'granted_at' =&amp;gt; $granted ? now() : null,
            'revoked_at' =&amp;gt; !$granted ? now() : null,
            'ip_address' =&amp;gt; $ipAddress,
        ]);
    }

    public function hasActiveConsent(string $patientId, string $consentType): bool
    {
        $latest = ConsentRecord::where('patient_id', $patientId)
            -&amp;gt;where('consent_type', $consentType)-&amp;gt;latest()-&amp;gt;first();
        return $latest &amp;amp;&amp;amp; $latest-&amp;gt;granted &amp;amp;&amp;amp; $latest-&amp;gt;revoked_at === null;
    }

    public function revokeConsent(string $patientId, string $consentType): void
    {
        $latest = ConsentRecord::where('patient_id', $patientId)
            -&amp;gt;where('consent_type', $consentType)
            -&amp;gt;where('granted', true)-&amp;gt;whereNull('revoked_at')-&amp;gt;latest()-&amp;gt;first();

        if ($latest) {
            $latest-&amp;gt;update(['revoked_at' =&amp;gt; now()]);
            DataAnonymisationJob::dispatch($patientId, $consentType)-&amp;gt;onQueue('high');
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;production-results-1&quot;&gt;Production Results&lt;/h3&gt;
&lt;p&gt;After implementing security-by-design across healthcare data platforms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Security audit findings (critical) &lt;/td&gt;&lt;td&gt;7 &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data access without audit trail &lt;/td&gt;&lt;td&gt;~15% of operations &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unauthorised access attempts detected &lt;/td&gt;&lt;td&gt;Unknown &lt;/td&gt;&lt;td&gt;23/month (all blocked)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Compliance audit preparation time &lt;/td&gt;&lt;td&gt;4 weeks &lt;/td&gt;&lt;td&gt;3 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data breach incidents &lt;/td&gt;&lt;td&gt;N/A &lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mean time to detect anomalous access &lt;/td&gt;&lt;td&gt;Unknown &lt;/td&gt;&lt;td&gt;&amp;lt; 15 minutes&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways-2&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Security is architecture, not a feature.&lt;/strong&gt; It must be built into every layer from day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Log every data access.&lt;/strong&gt; In healthcare, the question is not &quot;if&quot; an audit will happen but &quot;when.&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encrypt sensitive fields, not just the database.&lt;/strong&gt; Field-level encryption protects against application-layer breaches and insider threats.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implement RBAC with conditions.&lt;/strong&gt; &quot;Doctor&quot; is not a sufficient access level — conditions like assigned patients, working hours, and known IPs matter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Manage consent as a first-class data model.&lt;/strong&gt; Consent is a versioned, auditable, revocable record — not a checkbox.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan for data retention from the start.&lt;/strong&gt; Build purging infrastructure before you have millions of records.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building healthcare data platforms requires a security mindset that goes beyond typical web application security. Every design decision — from database schema to API design to deployment configuration — must account for the sensitivity of the data and the regulatory requirements that govern it.&lt;/p&gt;
&lt;p&gt;The patterns in this article are not theoretical. They come from building systems that handle real patient data under real regulatory scrutiny. Whether you build in Laravel or Node.js, the principles are universal: encrypt by default, log everything, enforce access at every layer, and design for the audit that will inevitably come.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16264/security-by-design-in-healthcare-data-platforms</guid>
<pubDate>Thu, 30 Apr 2026 12:09:49 +0000</pubDate>
</item>
<item>
<title>From Monolith to Modular: A Strangler Migration Playbook</title>
<link>https://coderlegion.com/16070/from-monolith-to-modular-a-strangler-migration-playbook</link>
<description>&lt;p&gt;Every successful application eventually outgrows its original architecture. What started as a clean, well-structured monolith becomes a tangled web of dependencies where changing one feature risks breaking three others. Deployments become risky, test suites take forever, and developers spend more time navigating the codebase than writing new features.&lt;/p&gt;
&lt;p&gt;The instinct is to rewrite. Do not. Rewrites are how engineering teams burn months of effort and deliver something that, at best, does what the old system already did. The better approach is incremental migration: systematically extracting capabilities from the monolith while keeping the system running in production the entire time.&lt;/p&gt;
&lt;p&gt;I have led these migrations across multiple companies — taking legacy codebases with 15+ modules, hundreds of database tables, and years of accumulated technical debt and transforming them into modular, maintainable architectures without downtime. This article is the playbook.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-monoliths-become-painful&quot;&gt;Why Monoliths Become Painful&lt;/h3&gt;
&lt;p&gt;Monoliths are not inherently bad. They are fast to build, easy to deploy, and simple to reason about — at small scale. The problems emerge gradually:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deployment coupling:&lt;/strong&gt; Every change, no matter how small, requires deploying the entire application. A CSS fix and a payment logic change ship in the same deployment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dependency tangles:&lt;/strong&gt; Module A depends on Module B which depends on Module C which depends on Module A. Circular dependencies make changes unpredictable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test suite slowdown:&lt;/strong&gt; Running the full test suite takes 30+ minutes. Developers start skipping tests. Bugs escape to production.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Team bottlenecks:&lt;/strong&gt; Multiple teams working on the same codebase create merge conflicts, code review backlogs, and release coordination overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scaling limitations:&lt;/strong&gt; You cannot scale the payment processing component independently of the user profile component. Everything scales (or fails) together.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-strangler-fig-pattern&quot;&gt;The Strangler Fig Pattern&lt;/h3&gt;
&lt;p&gt;The strangler fig pattern, named after the plant that gradually envelops its host tree, is the safest approach to migrating away from a monolith. The strategy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Identify a capability&lt;/strong&gt; to extract from the monolith&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build the new implementation&lt;/strong&gt; alongside the old one&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Route traffic&lt;/strong&gt; to the new implementation (gradually or all at once)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove the old implementation&lt;/strong&gt; once the new one is proven&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repeat&lt;/strong&gt; for the next capability&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The monolith continues to run throughout. There is no big bang cutover. Each migration step is small, reversible, and independently testable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-1-understanding-what-you-have&quot;&gt;Phase 1: Understanding What You Have&lt;/h3&gt;
&lt;p&gt;Before you extract anything, you need a map of the current system.&lt;/p&gt;
&lt;h4 id=&quot;dependency-analysis&quot;&gt;Dependency Analysis&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# Laravel: Find all cross-module dependencies
# Look for imports/use statements between modules
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;laravel-module-boundary-analysis&quot;&gt;Laravel Module Boundary Analysis&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class AnalyzeModuleDependencies extends Command
{
    protected $signature = 'modules:analyze';

    public function handle(): void
    {
        $modules = $this-&amp;gt;getModuleDirectories();
        $dependencies = [];

        foreach ($modules as $module) {
            $files = File::allFiles(app_path($module));
            $imports = [];

            foreach ($files as $file) {
                $content = $file-&amp;gt;getContents();
                preg_match_all('/use App\\\\(\w+)\\\\/', $content, $matches);
                $imports = array_merge($imports, $matches[1]);
            }

            $externalDeps = array_unique(
                array_filter($imports, fn ($dep) =&amp;gt; $dep !== $module)
            );

            $dependencies[$module] = $externalDeps;
        }

        foreach ($dependencies as $module =&amp;gt; $deps) {
            $this-&amp;gt;info(&quot;{$module} depends on: &quot; . implode(', ', $deps));
        }

        $this-&amp;gt;detectCircularDependencies($dependencies);
    }

    private function detectCircularDependencies(array $dependencies): void
    {
        foreach ($dependencies as $moduleA =&amp;gt; $depsA) {
            foreach ($depsA as $moduleB) {
                if (isset($dependencies[$moduleB]) &amp;amp;&amp;amp; in_array($moduleA, $dependencies[$moduleB])) {
                    $this-&amp;gt;warn(&quot;Circular dependency: {$moduleA} &amp;lt;-&amp;gt; {$moduleB}&quot;);
                }
            }
        }
    }

    private function getModuleDirectories(): array
    {
        return collect(File::directories(app_path()))
            -&amp;gt;map(fn ($dir) =&amp;gt; basename($dir))
            -&amp;gt;filter(fn ($dir) =&amp;gt; !in_array($dir, ['Console', 'Exceptions', 'Http', 'Providers']))
            -&amp;gt;values()
            -&amp;gt;all();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-dependency-mapping&quot;&gt;Node.js Dependency Mapping&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import fs from 'fs';
import path from 'path';

interface ModuleDependency {
  module: string;
  dependsOn: string[];
  files: number;
  linesOfCode: number;
}

function analyzeModuleDependencies(srcDir: string): ModuleDependency[] {
  const modules = fs.readdirSync(srcDir, { withFileTypes: true })
    .filter((d) =&amp;gt; d.isDirectory())
    .map((d) =&amp;gt; d.name);

  return modules.map((moduleName) =&amp;gt; {
    const moduleDir = path.join(srcDir, moduleName);
    const files = getAllFiles(moduleDir, ['.ts', '.js']);
    const imports: string[] = [];
    let totalLines = 0;

    files.forEach((file) =&amp;gt; {
      const content = fs.readFileSync(file, 'utf-8');
      totalLines += content.split('\n').length;

      const importRegex = /from\s+['&quot;]\.\.\/(\w+)/g;
      let match;
      while ((match = importRegex.exec(content)) !== null) {
        if (match[1] !== moduleName) {
          imports.push(match[1]);
        }
      }
    });

    return {
      module: moduleName,
      dependsOn: [...new Set(imports)],
      files: files.length,
      linesOfCode: totalLines,
    };
  });
}

function getAllFiles(dir: string, extensions: string[]): string[] {
  const results: string[] = [];
  const entries = fs.readdirSync(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      results.push(...getAllFiles(fullPath, extensions));
    } else if (extensions.some((ext) =&amp;gt; entry.name.endsWith(ext))) {
      results.push(fullPath);
    }
  }

  return results;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-2-establish-module-boundaries&quot;&gt;Phase 2: Establish Module Boundaries&lt;/h3&gt;
&lt;p&gt;Before extracting services, enforce boundaries within the monolith. This is the most important step and the one most teams skip.&lt;/p&gt;
&lt;h4 id=&quot;laravel-module-service-providers-and-contracts&quot;&gt;Laravel: Module Service Providers and Contracts&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// app/Billing/Contracts/BillingService.php
namespace App\Billing\Contracts;

interface BillingService
{
    public function createInvoice(string $customerId, array $lineItems): Invoice;
    public function processPayment(string $invoiceId, array $paymentMethod): PaymentResult;
    public function getCustomerBalance(string $customerId): Money;
}

// app/Billing/BillingServiceProvider.php
namespace App\Billing;

use Illuminate\Support\ServiceProvider;

class BillingServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this-&amp;gt;app-&amp;gt;bind(
            Contracts\BillingService::class,
            Services\BillingServiceImpl::class
        );
    }
}

// app/Orders/Services/OrderService.php
namespace App\Orders\Services;

use App\Billing\Contracts\BillingService;

class OrderService
{
    public function __construct(private BillingService $billing) {}

    public function completeOrder(Order $order): void
    {
        $invoice = $this-&amp;gt;billing-&amp;gt;createInvoice(
            $order-&amp;gt;customer_id,
            $order-&amp;gt;lineItems-&amp;gt;toArray()
        );

        $order-&amp;gt;update(['invoice_id' =&amp;gt; $invoice-&amp;gt;id, 'status' =&amp;gt; 'invoiced']);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-module-interfaces&quot;&gt;Node.js: Module Interfaces&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// src/billing/contracts.ts
export interface BillingService {
  createInvoice(customerId: string, lineItems: LineItem[]): Promise&amp;lt;Invoice&amp;gt;;
  processPayment(invoiceId: string, method: PaymentMethod): Promise&amp;lt;PaymentResult&amp;gt;;
  getCustomerBalance(customerId: string): Promise&amp;lt;Money&amp;gt;;
}

// src/billing/index.ts
export { BillingServiceImpl as BillingService } from './services/billing-service';
export type { BillingService as BillingServiceInterface } from './contracts';

// src/orders/services/order-service.ts
import type { BillingService } from '../../billing/contracts';

export class OrderService {
  constructor(private billing: BillingService) {}

  async completeOrder(order: Order): Promise&amp;lt;void&amp;gt; {
    const invoice = await this.billing.createInvoice(
      order.customerId,
      order.lineItems
    );

    await this.orderRepo.update(order.id, {
      invoiceId: invoice.id,
      status: 'invoiced',
    });
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key insight: modules communicate through interfaces, not direct implementation references. When you later extract the billing module into its own service, you replace the implementation behind the interface. The calling code does not change.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-3-extract-the-first-service&quot;&gt;Phase 3: Extract the First Service&lt;/h3&gt;
&lt;p&gt;Choose a module that is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Well-bounded (few incoming dependencies)&lt;/li&gt;
&lt;li&gt;Independently valuable&lt;/li&gt;
&lt;li&gt;Not on the critical path (if the extraction has problems, it should not take down the whole system)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-routing-layer&quot;&gt;The Routing Layer&lt;/h4&gt;
&lt;p&gt;Use a proxy or feature flag to route traffic between the monolith and the new service:&lt;/p&gt;
&lt;h4 id=&quot;laravel-switchable-implementation&quot;&gt;Laravel: Switchable Implementation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// app/Billing/BillingServiceProvider.php
class BillingServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this-&amp;gt;app-&amp;gt;bind(Contracts\BillingService::class, function ($app) {
            $useNewService = config('features.billing_service_v2', false);

            if ($useNewService) {
                return new HttpBillingServiceClient(
                    config('services.billing.url'),
                    config('services.billing.api_key'),
                );
            }

            return new Services\BillingServiceImpl();
        });
    }
}

// The HTTP client talks to the extracted billing service
class HttpBillingServiceClient implements Contracts\BillingService
{
    public function __construct(
        private string $baseUrl,
        private string $apiKey,
    ) {}

    public function createInvoice(string $customerId, array $lineItems): Invoice
    {
        $response = Http::withToken($this-&amp;gt;apiKey)
            -&amp;gt;timeout(10)
            -&amp;gt;retry(3, 100)
            -&amp;gt;post(&quot;{$this-&amp;gt;baseUrl}/api/invoices&quot;, [
                'customer_id' =&amp;gt; $customerId,
                'line_items' =&amp;gt; $lineItems,
            ]);

        if ($response-&amp;gt;failed()) {
            throw new BillingServiceException(&quot;Failed to create invoice: &quot; . $response-&amp;gt;body());
        }

        return Invoice::fromArray($response-&amp;gt;json());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;node-js-strategy-pattern-switch&quot;&gt;Node.js: Strategy Pattern Switch&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import type { BillingService } from './contracts';
import { LocalBillingService } from './services/billing-service';
import { RemoteBillingService } from './services/remote-billing-service';

export function createBillingService(): BillingService {
  if (process.env.BILLING_SERVICE_V2 === 'true') {
    return new RemoteBillingService(
      process.env.BILLING_SERVICE_URL!,
      process.env.BILLING_SERVICE_API_KEY!
    );
  }

  return new LocalBillingService();
}

class RemoteBillingService implements BillingService {
  constructor(
    private baseUrl: string,
    private apiKey: string,
  ) {}

  async createInvoice(customerId: string, lineItems: LineItem[]): Promise&amp;lt;Invoice&amp;gt; {
    const response = await fetch(`${this.baseUrl}/api/invoices`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify({ customer_id: customerId, line_items: lineItems }),
    });

    if (!response.ok) {
      throw new Error(`Billing service error: ${response.status}`);
    }

    return response.json();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-4-data-migration-strategy&quot;&gt;Phase 4: Data Migration Strategy&lt;/h3&gt;
&lt;p&gt;The hardest part of service extraction is data ownership. When the billing module becomes its own service, who owns the billing tables?&lt;/p&gt;
&lt;h4 id=&quot;shared-database-phase&quot;&gt;Shared Database Phase&lt;/h4&gt;
&lt;p&gt;Initially, the new service reads from the same database as the monolith. This avoids data migration complexity during the initial extraction.&lt;/p&gt;
&lt;h4 id=&quot;data-sync-phase&quot;&gt;Data Sync Phase&lt;/h4&gt;
&lt;p&gt;Introduce event-based data synchronisation. The new service maintains its own datastore, kept in sync via events:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class BillingDataSyncListener
{
    public function handle(CustomerUpdated $event): void
    {
        Http::post(config('services.billing.url') . '/api/sync/customer', [
            'customer_id' =&amp;gt; $event-&amp;gt;customerId,
            'name' =&amp;gt; $event-&amp;gt;name,
            'email' =&amp;gt; $event-&amp;gt;email,
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;full-ownership-phase&quot;&gt;Full Ownership Phase&lt;/h4&gt;
&lt;p&gt;The new service owns its data completely. The monolith no longer writes to billing tables. All access goes through the service API.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-5-zero-downtime-cutover&quot;&gt;Phase 5: Zero-Downtime Cutover&lt;/h3&gt;
&lt;p&gt;The cutover from monolith to new service should be invisible to users:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Shadow mode:&lt;/strong&gt; Both implementations run. The new service processes requests but responses come from the monolith. Compare outputs for correctness.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canary rollout:&lt;/strong&gt; Route 5% of traffic to the new service. Monitor error rates, latency, and correctness. Gradually increase.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Full rollout:&lt;/strong&gt; Route 100% to the new service. Keep the monolith implementation for 2 weeks as a rollback option.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cleanup:&lt;/strong&gt; Remove the old implementation from the monolith.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class BillingServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this-&amp;gt;app-&amp;gt;bind(Contracts\BillingService::class, function () {
            $strategy = config('features.billing_migration_phase');

            return match ($strategy) {
                'monolith' =&amp;gt; new Services\BillingServiceImpl(),
                'shadow' =&amp;gt; new ShadowBillingService(
                    primary: new Services\BillingServiceImpl(),
                    shadow: new HttpBillingServiceClient(/*...*/),
                ),
                'canary' =&amp;gt; new CanaryBillingService(
                    legacy: new Services\BillingServiceImpl(),
                    modern: new HttpBillingServiceClient(/*...*/),
                    percentage: (int) config('features.billing_canary_percentage', 5),
                ),
                'modern' =&amp;gt; new HttpBillingServiceClient(/*...*/),
                default =&amp;gt; new Services\BillingServiceImpl(),
            };
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;measuring-migration-success&quot;&gt;Measuring Migration Success&lt;/h3&gt;
&lt;p&gt;Track these metrics throughout the migration:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Deployment frequency &lt;/td&gt;&lt;td&gt;Should increase as modules become independent&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Lead time for changes &lt;/td&gt;&lt;td&gt;Should decrease as codebase complexity drops&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Test suite runtime &lt;/td&gt;&lt;td&gt;Should decrease per module&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Incidents per deployment &lt;/td&gt;&lt;td&gt;Should decrease with smaller change sets&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mean time to recovery &lt;/td&gt;&lt;td&gt;Should decrease with independent rollback capability&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;production-results-from-past-migrations&quot;&gt;Production Results from Past Migrations&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before (Monolith) &lt;/th&gt;&lt;th&gt;After (Modular)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Deployment frequency &lt;/td&gt;&lt;td&gt;1–2 per week &lt;/td&gt;&lt;td&gt;3–5 per day&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Full test suite runtime &lt;/td&gt;&lt;td&gt;35 minutes &lt;/td&gt;&lt;td&gt;8 minutes (per module)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Average deploy risk score &lt;/td&gt;&lt;td&gt;High &lt;/td&gt;&lt;td&gt;Low (per module)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Time to onboard new developer &lt;/td&gt;&lt;td&gt;3 weeks &lt;/td&gt;&lt;td&gt;1 week&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Query execution time &lt;/td&gt;&lt;td&gt;1.8s average &lt;/td&gt;&lt;td&gt;180ms average (60% reduction)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;System performance &lt;/td&gt;&lt;td&gt;Baseline &lt;/td&gt;&lt;td&gt;40% improvement&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways-3&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never rewrite. Always migrate incrementally.&lt;/strong&gt; The strangler fig pattern lets you replace the monolith piece by piece while keeping the system running.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Establish boundaries before extracting.&lt;/strong&gt; Enforce module interfaces within the monolith first. This makes extraction a configuration change, not a rewrite.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Start with a low-risk module.&lt;/strong&gt; Pick something well-bounded and non-critical for your first extraction. Build confidence before tackling core business logic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use feature flags for cutover.&lt;/strong&gt; Shadow mode, canary rollouts, and instant rollback make the migration safe. Never do a big bang switchover.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Solve data ownership explicitly.&lt;/strong&gt; Shared database → event-based sync → full ownership is the safest data migration path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure continuously.&lt;/strong&gt; Deployment frequency, test runtime, and incident rate tell you whether the migration is actually improving things.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Migrating from a monolith to a modular architecture is not a technical project — it is an engineering discipline applied over weeks and months. The strangler fig pattern makes it safe. Interface-first module design makes it clean. Feature flags make it reversible. And continuous measurement makes it accountable.&lt;/p&gt;
&lt;p&gt;Whether you are working in Laravel or Node.js, the approach is the same: understand what you have, enforce boundaries, extract behind interfaces, migrate data carefully, and cut over gradually. The monolith did not become painful overnight, and the fix will not happen overnight either. But each step makes the system better, and the cumulative impact is transformative.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16070/from-monolith-to-modular-a-strangler-migration-playbook</guid>
<pubDate>Tue, 28 Apr 2026 14:20:43 +0000</pubDate>
</item>
<item>
<title>Built a Laravel-Inspired Framework in Node.js (Here’s Why) - NetPress</title>
<link>https://coderlegion.com/15978/built-a-laravel-inspired-framework-in-node-js-heres-why-netpress</link>
<description>&lt;p&gt;For years, I’ve been working with Laravel.&lt;/p&gt;
&lt;p&gt;It gave me something most Node.js ecosystems don’t:&lt;br&gt;
structure, clarity, and speed of development.&lt;/p&gt;
&lt;p&gt;But every time I switched to Node.js, I felt like I was rebuilding the same things:&lt;/p&gt;
&lt;p&gt;routing structure&lt;br&gt;
middleware flow&lt;br&gt;
validation&lt;br&gt;
auth patterns&lt;br&gt;
project organization&lt;/p&gt;
&lt;p&gt;Everything felt… fragmented.&lt;/p&gt;
&lt;p&gt;So I decided to build my own solution.&lt;/p&gt;
&lt;p&gt;The Problem&lt;/p&gt;
&lt;p&gt;Node.js is powerful, but:&lt;/p&gt;
&lt;p&gt;too many choices&lt;br&gt;
no clear “standard” architecture&lt;br&gt;
a lot of boilerplate&lt;br&gt;
every project looks different&lt;/p&gt;
&lt;p&gt;Compare that to Laravel:&lt;/p&gt;
&lt;p&gt;consistent structure&lt;br&gt;
batteries included&lt;br&gt;
clean developer experience&lt;/p&gt;
&lt;p&gt;That’s what I wanted in Node.&lt;/p&gt;
&lt;p&gt;The Idea&lt;/p&gt;
&lt;p&gt;Build a framework that feels like Laravel…&lt;br&gt;
but runs on Node.js.&lt;/p&gt;
&lt;p&gt;That’s how Netpress started.&lt;/p&gt;
&lt;p&gt;What I Focused On&lt;/p&gt;
&lt;p&gt;Instead of building “just another framework”, I focused on:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Predictable Structure&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every project follows a clear pattern:&lt;/p&gt;
&lt;p&gt;Controllers&lt;br&gt;
Middleware&lt;br&gt;
Services&lt;br&gt;
Models&lt;/p&gt;
&lt;p&gt;No chaos. No guessing.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clean Routing&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Grouping, middleware, prefixes… just like Laravel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;router.group(authMiddleware, (router) =&amp;gt; {
  router.get('/me', meHandler);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Middleware Pipeline&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Simple and powerful request flow:&lt;/p&gt;
&lt;p&gt;auth&lt;br&gt;
rate limiting&lt;br&gt;
logging&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Rendering Support&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can use:&lt;/p&gt;
&lt;p&gt;Vue&lt;br&gt;
React&lt;/p&gt;
&lt;p&gt;Server-driven or hybrid apps without overcomplication.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Developer Experience&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Commands, structure, and conventions that make you move fast:&lt;/p&gt;
&lt;p&gt;generate app&lt;br&gt;
setup rendering&lt;br&gt;
run migrations&lt;br&gt;
seed data&lt;br&gt;
Why Not Just Use Express?&lt;/p&gt;
&lt;p&gt;You can.&lt;/p&gt;
&lt;p&gt;But then you’ll rebuild:&lt;/p&gt;
&lt;p&gt;structure&lt;br&gt;
conventions&lt;br&gt;
patterns&lt;br&gt;
architecture decisions&lt;/p&gt;
&lt;p&gt;Again… and again.&lt;/p&gt;
&lt;p&gt;Netpress tries to remove that friction.&lt;/p&gt;
&lt;p&gt;A Real Example&lt;/p&gt;
&lt;p&gt;Instead of wiring everything manually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.use(authMiddleware);
app.get('/me', handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get a structured flow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;router.group(authMiddleware, (router) =&amp;gt; {
  router.get('/me', meHandler);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What I Learned&lt;/p&gt;
&lt;p&gt;Building this taught me:&lt;/p&gt;
&lt;p&gt;developers don’t just need tools, they need opinionated structure&lt;br&gt;
consistency beats flexibility in most real-world apps&lt;br&gt;
good DX is underrated&lt;/p&gt;
&lt;p&gt;Here’s the docs:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://admicaa.github.io/netpress-docs/&quot;&gt;https://admicaa.github.io/netpress-docs/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Would love to hear your thoughts.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15978/built-a-laravel-inspired-framework-in-node-js-heres-why-netpress</guid>
<pubDate>Mon, 27 Apr 2026 14:30:01 +0000</pubDate>
</item>
<item>
<title>Designing Audit-Proof Financial Workflows with Immutable Logs</title>
<link>https://coderlegion.com/15054/designing-audit-proof-financial-workflows-with-immutable-logs</link>
<description>&lt;hr&gt;
&lt;p&gt;In financial software, data does not just need to be correct — it needs to be provably correct. Regulators, auditors, and compliance teams do not take your word for it. They need an unbroken chain of evidence showing exactly what happened, when it happened, and who triggered it.&lt;/p&gt;
&lt;p&gt;I have built financial platforms where a missing audit trail meant weeks of manual reconciliation. I have also built systems where every state change was captured in an append-only log that could answer any auditor's question in seconds. The difference is not complexity — it is intentional design.&lt;/p&gt;
&lt;p&gt;This article covers how to build audit-proof financial workflows using immutable event logs, covering architecture decisions, implementation patterns in both Laravel and Node.js, and the operational practices that make the system trustworthy under scrutiny.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-mutable-records-break-under-audit&quot;&gt;Why Mutable Records Break Under Audit&lt;/h3&gt;
&lt;p&gt;Most web applications store the current state of a record. When an invoice status changes from &quot;pending&quot; to &quot;paid,&quot; the database row is updated in place. The previous state is gone.&lt;/p&gt;
&lt;p&gt;This works fine for most applications. For financial systems, it is a liability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No history:&lt;/strong&gt; You cannot prove what the invoice status was yesterday at 3pm.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No attribution:&lt;/strong&gt; You cannot tell who changed the status or why.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No reconciliation path:&lt;/strong&gt; When numbers do not match, there is no trail to follow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No tamper evidence:&lt;/strong&gt; A modified record looks identical to a legitimate one.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Financial regulations — PCI DSS, SOX, FCA guidelines — require that systems maintain a complete, tamper-evident record of all financial transactions and state changes. An UPDATE statement destroys exactly the evidence you are required to preserve.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-immutable-log-pattern&quot;&gt;The Immutable Log Pattern&lt;/h3&gt;
&lt;p&gt;The solution is to never update or delete financial records. Instead, every change is recorded as a new, append-only event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Event 1] Invoice #1042 CREATED — amount: £5,000 — by: user_42 — at: 2025-03-01T09:15:00Z
[Event 2] Invoice #1042 APPROVED — approved_by: user_15 — at: 2025-03-01T14:30:00Z
[Event 3] Invoice #1042 PAYMENT_RECEIVED — amount: £5,000 — ref: PAY_8821 — at: 2025-03-05T11:00:00Z
[Event 4] Invoice #1042 RECONCILED — matched_to: BANK_TXN_4401 — at: 2025-03-06T08:45:00Z
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The current state of the invoice is derived by replaying the events in order. The audit trail is the events themselves. Nothing is lost, nothing is overwritten.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;[Client Request]
       |
       v
[Application Layer]
  - Validate business rules
  - Create event record
       |
       v
[DB Transaction]
  - Write event to audit_events table (append-only)
  - Update materialised view / current-state table
       |
       v
[Audit Events Table]           [Current State Table]
  (immutable, append-only)      (derived, rebuildable)
       |
       v
[Async Consumers]
  - Compliance reporting
  - Reconciliation engine
  - Notification service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The critical design decision: the audit events table is the source of truth. The current-state table is a projection that can be rebuilt from events at any time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation-in-laravel&quot;&gt;Implementation in Laravel&lt;/h3&gt;
&lt;h4 id=&quot;audit-events-migration&quot;&gt;Audit Events Migration&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Schema::create('audit_events', function (Blueprint $table) {
    $table-&amp;gt;uuid('id')-&amp;gt;primary();
    $table-&amp;gt;string('aggregate_type');
    $table-&amp;gt;string('aggregate_id');
    $table-&amp;gt;unsignedBigInteger('sequence_number');
    $table-&amp;gt;string('event_type');
    $table-&amp;gt;json('payload');
    $table-&amp;gt;json('metadata');
    $table-&amp;gt;string('actor_id');
    $table-&amp;gt;string('actor_type');
    $table-&amp;gt;string('ip_address')-&amp;gt;nullable();
    $table-&amp;gt;string('checksum');
    $table-&amp;gt;timestamp('occurred_at');
    $table-&amp;gt;timestamp('created_at');

    $table-&amp;gt;unique(['aggregate_type', 'aggregate_id', 'sequence_number']);
    $table-&amp;gt;index(['aggregate_type', 'aggregate_id']);
    $table-&amp;gt;index('event_type');
    $table-&amp;gt;index('occurred_at');
    $table-&amp;gt;index('actor_id');
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;checksum&lt;/code&gt; column stores a hash of the event data plus the previous event's checksum, forming a hash chain. Any tampering with a historical record breaks the chain.&lt;/p&gt;
&lt;h4 id=&quot;audit-event-service&quot;&gt;Audit Event Service&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\Services;

use App\Models\AuditEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class AuditEventService
{
    public function record(
        string $aggregateType,
        string $aggregateId,
        string $eventType,
        array $payload,
        string $actorId,
        string $actorType = 'user',
        ?string $ipAddress = null,
    ): AuditEvent {
        return DB::transaction(function () use (
            $aggregateType, $aggregateId, $eventType,
            $payload, $actorId, $actorType, $ipAddress
        ) {
            $lastEvent = AuditEvent::where('aggregate_type', $aggregateType)
                -&amp;gt;where('aggregate_id', $aggregateId)
                -&amp;gt;orderByDesc('sequence_number')
                -&amp;gt;lockForUpdate()
                -&amp;gt;first();

            $sequenceNumber = $lastEvent ? $lastEvent-&amp;gt;sequence_number + 1 : 1;
            $previousChecksum = $lastEvent?-&amp;gt;checksum ?? 'GENESIS';

            $eventData = [
                'aggregate_type' =&amp;gt; $aggregateType,
                'aggregate_id' =&amp;gt; $aggregateId,
                'sequence_number' =&amp;gt; $sequenceNumber,
                'event_type' =&amp;gt; $eventType,
                'payload' =&amp;gt; $payload,
                'actor_id' =&amp;gt; $actorId,
                'occurred_at' =&amp;gt; now(),
            ];

            $checksum = hash('sha256', $previousChecksum . json_encode($eventData));

            return AuditEvent::create([
                'id' =&amp;gt; Str::uuid(),
                ...$eventData,
                'metadata' =&amp;gt; [
                    'previous_checksum' =&amp;gt; $previousChecksum,
                    'schema_version' =&amp;gt; '1.0',
                ],
                'actor_type' =&amp;gt; $actorType,
                'ip_address' =&amp;gt; $ipAddress,
                'checksum' =&amp;gt; $checksum,
                'created_at' =&amp;gt; now(),
            ]);
        });
    }

    public function verifyChain(string $aggregateType, string $aggregateId): bool
    {
        $events = AuditEvent::where('aggregate_type', $aggregateType)
            -&amp;gt;where('aggregate_id', $aggregateId)
            -&amp;gt;orderBy('sequence_number')
            -&amp;gt;get();

        $previousChecksum = 'GENESIS';

        foreach ($events as $event) {
            $eventData = [
                'aggregate_type' =&amp;gt; $event-&amp;gt;aggregate_type,
                'aggregate_id' =&amp;gt; $event-&amp;gt;aggregate_id,
                'sequence_number' =&amp;gt; $event-&amp;gt;sequence_number,
                'event_type' =&amp;gt; $event-&amp;gt;event_type,
                'payload' =&amp;gt; $event-&amp;gt;payload,
                'actor_id' =&amp;gt; $event-&amp;gt;actor_id,
                'occurred_at' =&amp;gt; $event-&amp;gt;occurred_at,
            ];

            $expectedChecksum = hash('sha256', $previousChecksum . json_encode($eventData));

            if ($expectedChecksum !== $event-&amp;gt;checksum) {
                return false;
            }

            $previousChecksum = $event-&amp;gt;checksum;
        }

        return true;
    }

    public function getHistory(
        string $aggregateType,
        string $aggregateId
    ): \Illuminate\Support\Collection {
        return AuditEvent::where('aggregate_type', $aggregateType)
            -&amp;gt;where('aggregate_id', $aggregateId)
            -&amp;gt;orderBy('sequence_number')
            -&amp;gt;get();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;using-it-in-a-financial-workflow&quot;&gt;Using It in a Financial Workflow&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class InvoiceService
{
    public function __construct(
        private AuditEventService $auditService,
    ) {}

    public function createInvoice(array $data, string $userId, string $ip): Invoice
    {
        return DB::transaction(function () use ($data, $userId, $ip) {
            $invoice = Invoice::create([
                'number' =&amp;gt; $this-&amp;gt;generateInvoiceNumber(),
                'client_id' =&amp;gt; $data['client_id'],
                'amount' =&amp;gt; $data['amount'],
                'currency' =&amp;gt; $data['currency'],
                'status' =&amp;gt; 'draft',
                'due_date' =&amp;gt; $data['due_date'],
            ]);

            $this-&amp;gt;auditService-&amp;gt;record(
                aggregateType: 'invoice',
                aggregateId: (string) $invoice-&amp;gt;id,
                eventType: 'invoice.created',
                payload: [
                    'number' =&amp;gt; $invoice-&amp;gt;number,
                    'amount' =&amp;gt; $invoice-&amp;gt;amount,
                    'currency' =&amp;gt; $invoice-&amp;gt;currency,
                    'client_id' =&amp;gt; $invoice-&amp;gt;client_id,
                ],
                actorId: $userId,
                ipAddress: $ip,
            );

            return $invoice;
        });
    }

    public function approveInvoice(Invoice $invoice, string $approverId, string $ip): Invoice
    {
        return DB::transaction(function () use ($invoice, $approverId, $ip) {
            $invoice-&amp;gt;update(['status' =&amp;gt; 'approved']);

            $this-&amp;gt;auditService-&amp;gt;record(
                aggregateType: 'invoice',
                aggregateId: (string) $invoice-&amp;gt;id,
                eventType: 'invoice.approved',
                payload: [
                    'previous_status' =&amp;gt; 'draft',
                    'new_status' =&amp;gt; 'approved',
                ],
                actorId: $approverId,
                ipAddress: $ip,
            );

            return $invoice-&amp;gt;fresh();
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation-in-node-js&quot;&gt;Implementation in Node.js&lt;/h3&gt;
&lt;h4 id=&quot;prisma-schema&quot;&gt;Prisma Schema&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-prisma&quot;&gt;model AuditEvent {
  id              String   @id @default(uuid())
  aggregateType   String   @map(&quot;aggregate_type&quot;)
  aggregateId     String   @map(&quot;aggregate_id&quot;)
  sequenceNumber  Int      @map(&quot;sequence_number&quot;)
  eventType       String   @map(&quot;event_type&quot;)
  payload         Json
  metadata        Json
  actorId         String   @map(&quot;actor_id&quot;)
  actorType       String   @map(&quot;actor_type&quot;) @default(&quot;user&quot;)
  ipAddress       String?  @map(&quot;ip_address&quot;)
  checksum        String
  occurredAt      DateTime @map(&quot;occurred_at&quot;)
  createdAt       DateTime @default(now()) @map(&quot;created_at&quot;)

  @@unique([aggregateType, aggregateId, sequenceNumber])
  @@index([aggregateType, aggregateId])
  @@index([eventType])
  @@index([occurredAt])
  @@index([actorId])
  @@map(&quot;audit_events&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;audit-event-service-1&quot;&gt;Audit Event Service&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { PrismaClient } from '@prisma/client';
import crypto from 'crypto';

const prisma = new PrismaClient();

interface RecordEventParams {
  aggregateType: string;
  aggregateId: string;
  eventType: string;
  payload: Record&amp;lt;string, unknown&amp;gt;;
  actorId: string;
  actorType?: string;
  ipAddress?: string;
}

export async function recordAuditEvent(params: RecordEventParams) {
  return prisma.$transaction(async (tx) =&amp;gt; {
    const lastEvent = await tx.auditEvent.findFirst({
      where: {
        aggregateType: params.aggregateType,
        aggregateId: params.aggregateId,
      },
      orderBy: { sequenceNumber: 'desc' },
    });

    const sequenceNumber = lastEvent ? lastEvent.sequenceNumber + 1 : 1;
    const previousChecksum = lastEvent?.checksum ?? 'GENESIS';

    const eventData = {
      aggregate_type: params.aggregateType,
      aggregate_id: params.aggregateId,
      sequence_number: sequenceNumber,
      event_type: params.eventType,
      payload: params.payload,
      actor_id: params.actorId,
      occurred_at: new Date().toISOString(),
    };

    const checksum = crypto
      .createHash('sha256')
      .update(previousChecksum + JSON.stringify(eventData))
      .digest('hex');

    return tx.auditEvent.create({
      data: {
        aggregateType: params.aggregateType,
        aggregateId: params.aggregateId,
        sequenceNumber,
        eventType: params.eventType,
        payload: params.payload,
        metadata: {
          previous_checksum: previousChecksum,
          schema_version: '1.0',
        },
        actorId: params.actorId,
        actorType: params.actorType ?? 'user',
        ipAddress: params.ipAddress,
        checksum,
        occurredAt: new Date(),
      },
    });
  });
}

export async function verifyAuditChain(
  aggregateType: string,
  aggregateId: string
): Promise&amp;lt;boolean&amp;gt; {
  const events = await prisma.auditEvent.findMany({
    where: { aggregateType, aggregateId },
    orderBy: { sequenceNumber: 'asc' },
  });

  let previousChecksum = 'GENESIS';

  for (const event of events) {
    const eventData = {
      aggregate_type: event.aggregateType,
      aggregate_id: event.aggregateId,
      sequence_number: event.sequenceNumber,
      event_type: event.eventType,
      payload: event.payload,
      actor_id: event.actorId,
      occurred_at: event.occurredAt.toISOString(),
    };

    const expectedChecksum = crypto
      .createHash('sha256')
      .update(previousChecksum + JSON.stringify(eventData))
      .digest('hex');

    if (expectedChecksum !== event.checksum) {
      return false;
    }

    previousChecksum = event.checksum;
  }

  return true;
}

export async function getAggregateHistory(
  aggregateType: string,
  aggregateId: string
) {
  return prisma.auditEvent.findMany({
    where: { aggregateType, aggregateId },
    orderBy: { sequenceNumber: 'asc' },
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;express-route-for-audit-queries&quot;&gt;Express Route for Audit Queries&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import { Router } from 'express';

const router = Router();

router.get('/audit/:aggregateType/:aggregateId', async (req, res) =&amp;gt; {
  const { aggregateType, aggregateId } = req.params;

  const history = await getAggregateHistory(aggregateType, aggregateId);

  res.json({
    aggregate: { type: aggregateType, id: aggregateId },
    eventCount: history.length,
    events: history.map((e) =&amp;gt; ({
      sequence: e.sequenceNumber,
      type: e.eventType,
      payload: e.payload,
      actor: e.actorId,
      occurredAt: e.occurredAt,
    })),
  });
});

router.get('/audit/:aggregateType/:aggregateId/verify', async (req, res) =&amp;gt; {
  const { aggregateType, aggregateId } = req.params;
  const valid = await verifyAuditChain(aggregateType, aggregateId);

  res.json({
    aggregate: { type: aggregateType, id: aggregateId },
    chainValid: valid,
    verifiedAt: new Date().toISOString(),
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;retention-and-archival-strategy&quot;&gt;Retention and Archival Strategy&lt;/h3&gt;
&lt;p&gt;Audit logs grow fast. A system processing 10,000 transactions per day generates millions of events per year. You need a retention strategy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hot storage (0–90 days):&lt;/strong&gt; Primary database. Full query capability. Used for day-to-day operations and recent audits.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Warm storage (90 days–2 years):&lt;/strong&gt; Move older events to a read-optimised store (e.g., partitioned table, read replica, or columnar database). Still queryable but not on the critical path.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cold storage (2+ years):&lt;/strong&gt; Archive to object storage (S3, GCS) in a structured format (Parquet, JSON Lines). Retained for regulatory compliance periods (typically 5–7 years for financial data).&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;laravel-archival-command&quot;&gt;Laravel Archival Command&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ArchiveAuditEvents extends Command
{
    protected $signature = 'audit:archive {--older-than=90}';

    public function handle(): void
    {
        $cutoff = now()-&amp;gt;subDays((int) $this-&amp;gt;option('older-than'));

        $events = AuditEvent::where('occurred_at', '&amp;lt;', $cutoff)
            -&amp;gt;orderBy('occurred_at')
            -&amp;gt;cursor();

        $batch = [];
        $batchCount = 0;

        foreach ($events as $event) {
            $batch[] = $event-&amp;gt;toArray();

            if (count($batch) &amp;gt;= 10000) {
                $this-&amp;gt;uploadBatch($batch, $batchCount);
                $batch = [];
                $batchCount++;
            }
        }

        if (!empty($batch)) {
            $this-&amp;gt;uploadBatch($batch, $batchCount);
        }

        AuditEvent::where('occurred_at', '&amp;lt;', $cutoff)-&amp;gt;delete();

        $this-&amp;gt;info(&quot;Archived and purged events older than {$cutoff-&amp;gt;toDateString()}.&quot;);
    }

    private function uploadBatch(array $batch, int $index): void
    {
        $filename = sprintf('audit-archive/%s/batch-%04d.json', now()-&amp;gt;format('Y/m'), $index);

        Storage::disk('s3')-&amp;gt;put(
            $filename,
            collect($batch)-&amp;gt;map(fn ($e) =&amp;gt; json_encode($e))-&amp;gt;implode(&quot;\n&quot;)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;reconciliation-with-immutable-logs&quot;&gt;Reconciliation with Immutable Logs&lt;/h3&gt;
&lt;p&gt;One of the most valuable outcomes of immutable logs is automated reconciliation. Instead of manually comparing spreadsheets, you can programmatically verify that every financial event has a matching counterpart:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ReconciliationService
{
    public function reconcilePayments(string $date): array
    {
        $internalEvents = AuditEvent::where('event_type', 'payment.completed')
            -&amp;gt;whereDate('occurred_at', $date)
            -&amp;gt;get()
            -&amp;gt;keyBy(fn ($e) =&amp;gt; $e-&amp;gt;payload['gateway_reference']);

        $bankStatements = $this-&amp;gt;fetchBankStatements($date);

        $matched = [];
        $unmatchedInternal = [];
        $unmatchedBank = [];

        foreach ($bankStatements as $statement) {
            if ($internalEvents-&amp;gt;has($statement-&amp;gt;reference)) {
                $matched[] = $statement-&amp;gt;reference;
                $internalEvents-&amp;gt;forget($statement-&amp;gt;reference);
            } else {
                $unmatchedBank[] = $statement;
            }
        }

        $unmatchedInternal = $internalEvents-&amp;gt;values()-&amp;gt;all();

        return [
            'date' =&amp;gt; $date,
            'matched_count' =&amp;gt; count($matched),
            'unmatched_internal' =&amp;gt; count($unmatchedInternal),
            'unmatched_bank' =&amp;gt; count($unmatchedBank),
            'discrepancies' =&amp;gt; [...$unmatchedInternal, ...$unmatchedBank],
        ];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;production-results-2&quot;&gt;Production Results&lt;/h3&gt;
&lt;p&gt;After implementing immutable audit logging across financial platforms:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Reconciliation time (weekly) &lt;/td&gt;&lt;td&gt;4+ hours manual &lt;/td&gt;&lt;td&gt;15 minutes automated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Audit preparation time &lt;/td&gt;&lt;td&gt;2–3 weeks &lt;/td&gt;&lt;td&gt;2 days&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unresolved discrepancies &lt;/td&gt;&lt;td&gt;8–12 per month &lt;/td&gt;&lt;td&gt;0–1 per month&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Compliance audit findings &lt;/td&gt;&lt;td&gt;Multiple per review &lt;/td&gt;&lt;td&gt;Zero material findings&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data dispute resolution time &lt;/td&gt;&lt;td&gt;3–5 days &lt;/td&gt;&lt;td&gt;&amp;lt; 1 hour&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways-4&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never update financial records in place.&lt;/strong&gt; Append new events instead. The current state is a projection of the event history.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use hash chains for tamper evidence.&lt;/strong&gt; Each event's checksum includes the previous event's checksum. Any modification breaks the chain.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Record who, what, when, and why.&lt;/strong&gt; Every event needs an actor, a timestamp, and the business context. IP addresses and session identifiers add forensic value.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for retention from day one.&lt;/strong&gt; Audit logs grow quickly. Tiered storage (hot/warm/cold) keeps costs manageable while satisfying compliance requirements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automate reconciliation.&lt;/strong&gt; Immutable logs enable programmatic reconciliation that replaces hours of manual spreadsheet comparison.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Write events and business data in the same transaction.&lt;/strong&gt; If they are not atomic, you risk inconsistency between your source of truth and your audit trail.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Audit-proof financial workflows are not about generating more logs — they are about designing systems where every state change is captured, chained, and verifiable. Append-only writes, hash-chain integrity, and tiered retention create a system that answers any auditor's question with evidence, not explanations.&lt;/p&gt;
&lt;p&gt;Whether you build in Laravel or Node.js, the patterns are the same. This infrastructure pays dividends every time a regulator asks a question or a discrepancy needs resolution.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15054/designing-audit-proof-financial-workflows-with-immutable-logs</guid>
<pubDate>Fri, 17 Apr 2026 10:09:35 +0000</pubDate>
</item>
<item>
<title>Building Crypto Payments into Telegram: A Technical Deep Dive into telegram-wallet-php</title>
<link>https://coderlegion.com/14893/building-crypto-payments-into-telegram-a-technical-deep-dive-into-telegram-wallet-php</link>
<description>&lt;p&gt;Telegram is no longer just a messaging app. With an audience exceeding 1 billion monthly active users by early 2025 and deep integration with the TON blockchain, the platform has become serious infrastructure for commerce and decentralized applications. Developers building bots and Telegram Mini Apps (TMAs) now have a genuine opportunity to monetize their products by accepting cryptocurrency payments directly within the chat interface.&lt;/p&gt;
&lt;p&gt;The official Wallet Pay API makes this possible, allowing merchants to accept TON, USDT, BTC, and NOT from users without redirecting them to external sites. However, integrating a raw REST API is never trivial. A developer must implement authentication, construct request payloads, verify cryptographic webhook signatures, and map HTTP status codes to meaningful errors.&lt;/p&gt;
&lt;p&gt;This is exactly the problem that &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tigusigalpa/telegram-wallet-php&quot;&gt;telegram-wallet-php&lt;/a&gt; solves. Created by Igor Sazonov, this open-source PHP SDK wraps the Wallet Pay API v1.2.0 in a clean, modern, and strictly typed interface. The library works as a standalone solution in any PHP 8.1+ project and provides first-class integration for Laravel (versions 9 through 13). &lt;/p&gt;
&lt;p&gt;This post is a technical walkthrough of the current implementation: architectural shape, webhook security, error handling, and production gotchas.&lt;/p&gt;
&lt;h4 id=&quot;architectural-shape-and-typing&quot;&gt;Architectural Shape and Typing&lt;/h4&gt;
&lt;p&gt;At code level, &lt;code&gt;telegram-wallet-php&lt;/code&gt; follows a fairly direct layered structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Contracts (interfaces) for the main client&lt;/li&gt;
&lt;li&gt;Data Transfer Objects (DTOs) for request payloads and API responses&lt;/li&gt;
&lt;li&gt;PHP 8.1 Enums for statuses and event types&lt;/li&gt;
&lt;li&gt;Typed exceptions mapped to HTTP status codes&lt;/li&gt;
&lt;li&gt;Laravel integration (Service Provider, Facade, Middleware)&lt;/li&gt;
&lt;li&gt;Webhook handling classes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In practical terms, the main entry point is the &lt;code&gt;WalletPayClient&lt;/code&gt; class, which implements &lt;code&gt;WalletPayClientInterface&lt;/code&gt;. Its constructor accepts a PSR-18 compatible HTTP client, which is critical for unit testing — you can inject a mock client to verify your application logic without making real HTTP requests.&lt;/p&gt;
&lt;p&gt;The library heavily uses DTOs to represent both request payloads and API responses. This is a significant advantage over an array-based approach. Using named arguments (a PHP 8.0 feature) makes the intent of each parameter crystal clear, and your IDE will warn you if you pass the wrong type.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$request = new CreateOrderRequest(
    amount: new MoneyAmount('USD', '9.99'),
    description: 'Premium subscription',
    externalId: 'ORDER-123',
    timeoutSeconds: 3600,
    customerTelegramUserId: 123456789
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Order statuses and webhook event types are represented as PHP 8.1 Enums, eliminating the risk of typos when comparing strings. The order lifecycle follows a clear state machine: from &lt;code&gt;ACTIVE&lt;/code&gt; (awaiting payment) to &lt;code&gt;PAID&lt;/code&gt; (successful payment), &lt;code&gt;EXPIRED&lt;/code&gt; (timeout reached), or &lt;code&gt;CANCELLED&lt;/code&gt; (aborted).&lt;/p&gt;
&lt;h4 id=&quot;webhook-security-the-critical-path&quot;&gt;Webhook Security: The Critical Path&lt;/h4&gt;
&lt;p&gt;Webhooks are the mechanism by which Wallet Pay notifies your server about payment outcomes. Implementing this correctly is the most security-sensitive part of any payment integration.&lt;/p&gt;
&lt;p&gt;When Wallet Pay sends a webhook, it includes two headers: &lt;code&gt;WalletPay-Timestamp&lt;/code&gt; (a nanosecond timestamp) and &lt;code&gt;WalletPay-Signature&lt;/code&gt; (a Base64-encoded HMAC-SHA256 signature). The signature is computed based on the HTTP method, URI path, timestamp, and request body.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;WebhookVerifier&lt;/code&gt; class handles all this cryptography automatically. The developer does not need to implement HMAC verification manually, which is a major security benefit, as manual HMAC implementations are frequently a source of vulnerabilities.&lt;/p&gt;
&lt;p&gt;In Laravel, the package provides a ready-to-use &lt;code&gt;walletpay.webhook&lt;/code&gt; Middleware. Once registered, the webhook routing becomes extremely concise: the Middleware verifies the signature, and the controller simply parses the events and updates the database state.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::post('/webhook/walletpay', function (Request $request, WebhookVerifier $verifier) {
    $events = $verifier-&amp;gt;parseWebhookEvents($request-&amp;gt;getContent());
    
    foreach ($events as $event) {
        if ($event-&amp;gt;type === WebhookEventType::ORDER_PAID) {
            // Payment successful! Update status in DB
        }
    }
    
    return response('OK', 200);
})-&amp;gt;middleware('walletpay.webhook');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;error-handling-typed-exceptions&quot;&gt;Error Handling: Typed Exceptions&lt;/h4&gt;
&lt;p&gt;One of the most developer-friendly aspects of &lt;code&gt;telegram-wallet-php&lt;/code&gt; is its exception hierarchy. Instead of catching a generic &lt;code&gt;\Exception&lt;/code&gt; and trying to parse the error message, you can precisely handle each failure scenario.&lt;/p&gt;
&lt;p&gt;The exception hierarchy maps directly to the HTTP status codes defined in the Wallet Pay API documentation. For example, &lt;code&gt;InvalidRequestException&lt;/code&gt; corresponds to status 400, &lt;code&gt;RateLimitException&lt;/code&gt; to 429 (allowing you to implement exponential backoff and retries), and &lt;code&gt;InvalidWebhookSignatureException&lt;/code&gt; is thrown when webhook signature verification fails.&lt;/p&gt;
&lt;h4 id=&quot;important-production-gotchas&quot;&gt;Important Production Gotchas&lt;/h4&gt;
&lt;p&gt;Based on the documentation and source code, there are several important considerations for production deployment:&lt;/p&gt;
&lt;p&gt;First, the &lt;code&gt;customerTelegramUserId&lt;/code&gt; field is mandatory, and only the specified Telegram user can pay for the order. This is a security feature that prevents payment link sharing, but it means you cannot create a generic &quot;pay now&quot; link — you must create a unique order for each user.&lt;/p&gt;
&lt;p&gt;Second, the &lt;code&gt;externalId&lt;/code&gt; field serves as an idempotency key. If you create an order with the same &lt;code&gt;externalId&lt;/code&gt; twice (for example, due to a retry on a network timeout), the API will return the existing order rather than creating a duplicate. You should generate your &lt;code&gt;externalId&lt;/code&gt; deterministically.&lt;/p&gt;
&lt;p&gt;Third, the Wallet Pay signature verification includes the exact URI path of the webhook endpoint. The path must match exactly what you configured in your store settings, including the presence or absence of a trailing slash. A mismatch will cause all webhook verifications to fail.&lt;/p&gt;
&lt;h4 id=&quot;the-bottom-line&quot;&gt;The Bottom Line&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;telegram-wallet-php&lt;/code&gt; library is a well-designed, production-ready SDK that significantly lowers the barrier to integrating cryptocurrency payments into PHP applications and Telegram bots. Leveraging modern PHP 8.1 features (Enums, readonly properties, named arguments, and typed DTOs) makes the code safer, more readable, and easier to maintain.&lt;/p&gt;
&lt;p&gt;The first-class Laravel integration is a genuine competitive advantage. The combination of a Service Provider, Facade, and Middleware means that Laravel developers can add Telegram crypto payments to their applications with minimal boilerplate, following conventions they already know. The built-in webhook signature verification eliminates a common source of vulnerabilities in payment integrations.&lt;/p&gt;
&lt;p&gt;For developers building products in the rapidly growing Telegram Mini App ecosystem, this library is an excellent foundation.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14893/building-crypto-payments-into-telegram-a-technical-deep-dive-into-telegram-wallet-php</guid>
<pubDate>Wed, 15 Apr 2026 12:56:51 +0000</pubDate>
</item>
<item>
<title>Engineering Real-Time Delivery Tracking That Increased Checkout Conversions by 20%</title>
<link>https://coderlegion.com/14376/engineering-real-time-delivery-tracking-that-increased-checkout-conversions-by-20</link>
<description>&lt;p&gt;In e-commerce and on-demand delivery, the moment a user clicks &quot;Place Order&quot; begins an anxiety cycle. Where is my order? When will it arrive? This uncertainty kills conversions.&lt;/p&gt;
&lt;p&gt;Between November 2020 and July 2021, I worked as a Full-Stack Developer at Viaduct, where I engineered a real-time delivery tracking system for an on-demand platform serving 5,000+ active users. The platform connected customers with local vendors for same-day delivery. Before I joined, users were abandoning carts and cancelling orders at alarming rates because they had zero visibility into delivery status.&lt;/p&gt;
&lt;p&gt;This article details how I designed and built the tracking system — Google Maps integration, WebSocket-powered live updates, a Vue.js reactive interface, and the Laravel backend tying it together. The result: a 20% increase in checkout conversions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-delivery-opacity-was-killing-revenue&quot;&gt;The Problem: Delivery Opacity Was Killing Revenue&lt;/h3&gt;
&lt;p&gt;When I joined Viaduct, the delivery platform was functional but opaque. Users placed orders, received a confirmation email, and waited. The only updates were manual — a dispatcher marked orders &quot;out for delivery&quot; when a driver departed and &quot;delivered&quot; when reported back.&lt;/p&gt;
&lt;p&gt;The consequences were measurable:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cart abandonment:&lt;/strong&gt; 32% of users abandoned at checkout. Exit surveys showed the top reason was &quot;uncertain delivery time.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-order cancellations:&lt;/strong&gt; 15% of completed checkouts were cancelled within 30 minutes, most citing &quot;no delivery updates.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Support overload:&lt;/strong&gt; 200+ daily calls asking &quot;where is my order?&quot; — each averaging 4 minutes, consuming 13 person-hours daily.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Driver accountability gaps:&lt;/strong&gt; Without GPS tracking, there was no way to verify delivery times, identify route inefficiencies, or resolve disputes.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;my-role-full-stack-architect-of-the-tracking-system&quot;&gt;My Role: Full-Stack Architect of the Tracking System&lt;/h3&gt;
&lt;p&gt;I designed and built the entire real-time tracking system end-to-end: the Laravel backend infrastructure, Google Maps API integration, the WebSocket communication layer, and the Vue.js frontend tracking interface. I owned it from initial technical design through production deployment and post-launch optimisation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technical-deep-dive-building-the-real-time-tracking-infrastructure&quot;&gt;Technical Deep Dive: Building the Real-Time Tracking Infrastructure&lt;/h3&gt;
&lt;h4 id=&quot;1-driver-location-collection-architecture&quot;&gt;1. Driver Location Collection Architecture&lt;/h4&gt;
&lt;p&gt;The foundation of real-time tracking is reliable location data. I built a location reporting service on drivers' devices, transmitting GPS coordinates at configurable intervals.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class DriverLocationController extends Controller
{
    public function updateLocation(UpdateLocationRequest $request): JsonResponse
    {
        $driver = $request-&amp;gt;user();
        $validated = $request-&amp;gt;validated();

        $location = DriverLocation::create([
            'driver_id' =&amp;gt; $driver-&amp;gt;id,
            'latitude' =&amp;gt; $validated['latitude'],
            'longitude' =&amp;gt; $validated['longitude'],
            'accuracy' =&amp;gt; $validated['accuracy'],
            'speed' =&amp;gt; $validated['speed'] ?? null,
            'heading' =&amp;gt; $validated['heading'] ?? null,
            'recorded_at' =&amp;gt; Carbon::createFromTimestamp($validated['timestamp']),
        ]);

        if ($activeDelivery = $driver-&amp;gt;activeDelivery) {
            $this-&amp;gt;broadcastLocationUpdate($activeDelivery, $location);
            $this-&amp;gt;updateEstimatedArrival($activeDelivery, $location);
        }

        return response()-&amp;gt;json(['status' =&amp;gt; 'recorded'], 200);
    }

    private function broadcastLocationUpdate(Delivery $delivery, DriverLocation $location): void
    {
        Redis::publish(&quot;delivery:{$delivery-&amp;gt;id}:location&quot;, json_encode([
            'delivery_id' =&amp;gt; $delivery-&amp;gt;id,
            'driver' =&amp;gt; [
                'latitude' =&amp;gt; $location-&amp;gt;latitude,
                'longitude' =&amp;gt; $location-&amp;gt;longitude,
                'heading' =&amp;gt; $location-&amp;gt;heading,
            ],
            'timestamp' =&amp;gt; $location-&amp;gt;recorded_at-&amp;gt;toIso8601String(),
        ]));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Location updates published to Redis channels, bridging the Laravel backend and the WebSocket server. This decoupled location ingestion (high-frequency HTTP POSTs from drivers) from location broadcasting (persistent WebSocket connections to customers), allowing each to scale independently.&lt;/p&gt;
&lt;h4 id=&quot;2-google-maps-integration-for-eta-calculation&quot;&gt;2. Google Maps Integration for ETA Calculation&lt;/h4&gt;
&lt;p&gt;Accurate ETAs were critical. I integrated the Google Maps Directions API with live traffic data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class GoogleMapsService
{
    public static function getDirections(array $origin, array $destination): ?DirectionsResult
    {
        $response = Http::get('https://maps.googleapis.com/maps/api/directions/json', [
            'origin' =&amp;gt; &quot;{$origin['lat']},{$origin['lng']}&quot;,
            'destination' =&amp;gt; &quot;{$destination['lat']},{$destination['lng']}&quot;,
            'mode' =&amp;gt; 'driving',
            'departure_time' =&amp;gt; 'now',
            'traffic_model' =&amp;gt; 'best_guess',
            'key' =&amp;gt; config('services.google_maps.api_key'),
        ]);

        if (!$response-&amp;gt;successful() || $response-&amp;gt;json('status') !== 'OK') {
            return null;
        }

        $leg = $response-&amp;gt;json('routes.0.legs.0');
        return new DirectionsResult(
            durationMinutes: (int) ceil($leg['duration_in_traffic']['value'] / 60),
            distanceKm: round($leg['distance']['value'] / 1000, 1),
            polyline: $response-&amp;gt;json('routes.0.overview_polyline.points'),
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;departure_time=now&lt;/code&gt; and &lt;code&gt;traffic_model=best_guess&lt;/code&gt; ensured ETAs reflected live traffic — during rush hours, static estimates might show 10 minutes while actual time was 25. However, calling Google Maps on every location update (every 10 seconds per driver) would be prohibitively expensive. I implemented intelligent throttling using Haversine distance:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ETAService
{
    private const ETA_CACHE_TTL = 60;
    private const MIN_DISTANCE_FOR_RECALC = 0.2; // km

    public function shouldRecalculateETA(Delivery $delivery, DriverLocation $newLocation): bool
    {
        if (!$delivery-&amp;gt;eta_updated_at) return true;
        if (now()-&amp;gt;diffInSeconds($delivery-&amp;gt;eta_updated_at) &amp;lt; self::ETA_CACHE_TTL) return false;

        $lastCalcLocation = Cache::get(&quot;eta_calc_location:{$delivery-&amp;gt;id}&quot;);
        if (!$lastCalcLocation) return true;

        return $this-&amp;gt;haversineDistance(
            $lastCalcLocation['lat'], $lastCalcLocation['lng'],
            $newLocation-&amp;gt;latitude, $newLocation-&amp;gt;longitude
        ) &amp;gt;= self::MIN_DISTANCE_FOR_RECALC;
    }

    private function haversineDistance(float $lat1, float $lon1, float $lat2, float $lon2): float
    {
        $dLat = deg2rad($lat2 - $lat1);
        $dLon = deg2rad($lon2 - $lon1);
        $a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLon/2)**2;
        return 6371 * 2 * atan2(sqrt($a), sqrt(1 - $a));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ETAs were recalculated only when the driver moved 200m+ and 60+ seconds had elapsed, reducing Google Maps API calls by ~80%.&lt;/p&gt;
&lt;h4 id=&quot;3-websocket-server-for-live-customer-updates&quot;&gt;3. WebSocket Server for Live Customer Updates&lt;/h4&gt;
&lt;p&gt;The real-time frontend was powered by a Node.js WebSocket server subscribed to Redis channels:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const wss = new WebSocket.Server({ port: 6001 });
const subscriber = new Redis(process.env.REDIS_URL);
const deliverySubscriptions = new Map();

wss.on('connection', (ws) =&amp;gt; {
  ws.on('message', (raw) =&amp;gt; {
    const msg = JSON.parse(raw);
    if (msg.type === 'subscribe_delivery') {
      if (!deliverySubscriptions.has(msg.deliveryId)) {
        deliverySubscriptions.set(msg.deliveryId, new Set());
        subscriber.subscribe(
          `delivery:${msg.deliveryId}:location`,
          `delivery:${msg.deliveryId}:eta`,
          `delivery:${msg.deliveryId}:status`
        );
      }
      deliverySubscriptions.get(msg.deliveryId).add(ws);
    }
  });

  ws.on('close', () =&amp;gt; {
    if (ws.deliveryId) {
      const subs = deliverySubscriptions.get(ws.deliveryId);
      if (subs) { subs.delete(ws); if (subs.size === 0) deliverySubscriptions.delete(ws.deliveryId); }
    }
  });
});

subscriber.on('message', (channel, message) =&amp;gt; {
  const [, deliveryId, eventType] = channel.split(':');
  const subs = deliverySubscriptions.get(deliveryId);
  if (!subs) return;
  const payload = JSON.stringify({ type: `delivery_${eventType}`, data: JSON.parse(message) });
  for (const ws of subs) { if (ws.readyState === WebSocket.OPEN) ws.send(payload); }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server was deliberately stateless — it bridged Redis pub/sub to WebSocket connections with no business logic, making it horizontally scalable.&lt;/p&gt;
&lt;h4 id=&quot;4-vue-js-real-time-tracking-interface&quot;&gt;4. Vue.js Real-Time Tracking Interface&lt;/h4&gt;
&lt;p&gt;The customer-facing tracking page used Vue.js Composition API. The core was a WebSocket composable with exponential backoff reconnection:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;export function useDeliveryWebSocket(deliveryId) {
  const driverLocation = ref(null);
  const eta = ref(null);
  const deliveryStatus = ref('confirmed');
  let ws = null, reconnectAttempts = 0;

  function connect() {
    ws = new WebSocket(process.env.VUE_APP_WS_URL);
    ws.onopen = () =&amp;gt; {
      reconnectAttempts = 0;
      ws.send(JSON.stringify({ type: 'subscribe_delivery', deliveryId }));
    };
    ws.onmessage = ({ data }) =&amp;gt; {
      const msg = JSON.parse(data);
      if (msg.type === 'delivery_location') driverLocation.value = msg.data.driver;
      else if (msg.type === 'delivery_eta') eta.value = msg.data.eta_minutes;
      else if (msg.type === 'delivery_status') deliveryStatus.value = msg.data.status;
    };
    ws.onclose = () =&amp;gt; {
      if (reconnectAttempts &amp;lt; 10)
        setTimeout(connect, Math.min(1000 * 2 ** reconnectAttempts++, 30000));
    };
  }
  return { driverLocation, eta, deliveryStatus, connect, disconnect: () =&amp;gt; ws?.close() };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The map component initialised Google Maps, rendered destination and driver markers, and animated the driver marker between positions using cubic easing. Without animation, the marker would &quot;teleport&quot; every 10 seconds — with smooth interpolation, users perceived continuous movement, dramatically improving perceived quality.&lt;/p&gt;
&lt;h4 id=&quot;5-delivery-status-lifecycle-and-performance&quot;&gt;5. Delivery Status Lifecycle and Performance&lt;/h4&gt;
&lt;p&gt;Every status transition published to Redis for real-time updates, sent push notifications, and recorded metrics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class Delivery extends Model
{
    public function markPickedUp(): void
    {
        $this-&amp;gt;update(['status' =&amp;gt; 'picked_up', 'picked_up_at' =&amp;gt; now()]);
        Redis::publish(&quot;delivery:{$this-&amp;gt;id}:status&quot;, json_encode([
            'delivery_id' =&amp;gt; $this-&amp;gt;id, 'status' =&amp;gt; 'picked_up',
            'driver' =&amp;gt; ['name' =&amp;gt; $this-&amp;gt;driver-&amp;gt;name, 'phone' =&amp;gt; $this-&amp;gt;driver-&amp;gt;phone],
        ]));
        $this-&amp;gt;order-&amp;gt;customer-&amp;gt;notify(new DeliveryPickedUpNotification($this));
    }

    public function markDelivered(): void
    {
        $this-&amp;gt;update(['status' =&amp;gt; 'delivered', 'delivered_at' =&amp;gt; now()]);
        Redis::publish(&quot;delivery:{$this-&amp;gt;id}:status&quot;, json_encode([
            'delivery_id' =&amp;gt; $this-&amp;gt;id, 'status' =&amp;gt; 'delivered',
        ]));
        $this-&amp;gt;order-&amp;gt;customer-&amp;gt;notify(new DeliveryCompletedNotification($this));
        DeliveryMetric::create([
            'delivery_id' =&amp;gt; $this-&amp;gt;id, 'driver_id' =&amp;gt; $this-&amp;gt;driver_id,
            'estimated_minutes' =&amp;gt; $this-&amp;gt;estimated_arrival_minutes,
            'actual_minutes' =&amp;gt; $this-&amp;gt;picked_up_at?-&amp;gt;diffInMinutes($this-&amp;gt;delivered_at),
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;DeliveryMetric&lt;/code&gt; model enabled comparing estimated vs. actual delivery times, identifying underperforming drivers and slow routes.&lt;/p&gt;
&lt;p&gt;For performance at scale, I buffered location writes in Redis and flushed to PostgreSQL in batches every 30 seconds, reducing database write load by 90% while maintaining real-time delivery through the pub/sub layer. Vendor discovery pages used PostGIS spatial queries (&lt;code&gt;ST_DWithin&lt;/code&gt;, &lt;code&gt;ST_Distance&lt;/code&gt;) with aggressive caching, returning results in under 50ms across thousands of records.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-results-measurable-business-impact&quot;&gt;The Results: Measurable Business Impact&lt;/h3&gt;
&lt;p&gt;The system launched after eight weeks of development. Results over a 60-day observation period:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Before &lt;/th&gt;&lt;th&gt;After &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Checkout conversion rate &lt;/td&gt;&lt;td&gt;68% &lt;/td&gt;&lt;td&gt;88% &lt;/td&gt;&lt;td&gt;&lt;strong&gt;+20 percentage points&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Post-order cancellation rate &lt;/td&gt;&lt;td&gt;15% &lt;/td&gt;&lt;td&gt;4% &lt;/td&gt;&lt;td&gt;&lt;strong&gt;73% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Where is my order?&quot; support calls &lt;/td&gt;&lt;td&gt;200+/day &lt;/td&gt;&lt;td&gt;~35/day &lt;/td&gt;&lt;td&gt;&lt;strong&gt;82% reduction&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Customer satisfaction score &lt;/td&gt;&lt;td&gt;3.4/5 &lt;/td&gt;&lt;td&gt;4.3/5 &lt;/td&gt;&lt;td&gt;&lt;strong&gt;26% improvement&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Active platform users &lt;/td&gt;&lt;td&gt;~3,200 &lt;/td&gt;&lt;td&gt;5,000+ &lt;/td&gt;&lt;td&gt;&lt;strong&gt;56% growth&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Tracking page views per order &lt;/td&gt;&lt;td&gt;0 &lt;/td&gt;&lt;td&gt;4.2 &lt;/td&gt;&lt;td&gt;New engagement channel&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The support call reduction alone freed 11 person-hours daily — redirected to proactive customer outreach and vendor management.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technical-lessons&quot;&gt;Technical Lessons&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decouple ingestion from delivery.&lt;/strong&gt; Location collection (Laravel) and broadcasting (Node.js WebSocket) had different performance profiles. Separate services connected by Redis allowed independent scaling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Animate transitions, don't teleport.&lt;/strong&gt; Users perceive smooth movement as real-time tracking and position jumps as a broken system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throttle expensive API calls intelligently.&lt;/strong&gt; The Haversine check before ETA recalculation saved thousands of unnecessary Google Maps calls daily.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Buffer writes, stream reads.&lt;/strong&gt; Batch-writing to the database while streaming through Redis provided both persistence and live performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Measure business metrics.&lt;/strong&gt; WebSocket latency matters, but checkout conversion rate proved the system's value.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14376/engineering-real-time-delivery-tracking-that-increased-checkout-conversions-by-20</guid>
<pubDate>Thu, 09 Apr 2026 22:37:20 +0000</pubDate>
</item>
<item>
<title>Don’t Pass Array or Variables to Laravel Blade Views Instead Do This</title>
<link>https://coderlegion.com/14222/dont-pass-array-or-variables-to-laravel-blade-views-instead-do-this</link>
<description>&lt;p&gt;Are you a Laravel developer. Do you love Blade. It’s simple, expressive, and fits naturally with the framework. But let’s be honest, most projects eventually turn their Blade files into dumping grounds for random variables. The Controllers pass loose data, views end up receiving unstructured data, partials rely on implicit assumptions, and the whole setup becomes fragile and error-prone.&lt;/p&gt;
&lt;h3 id=&quot;the-result-&quot;&gt;The Result?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;No autocomplete.&lt;/li&gt;
&lt;li&gt;Runtime errors from typos.&lt;/li&gt;
&lt;li&gt;Partial views with zero contracts.&lt;/li&gt;
&lt;li&gt;A mess that only grows as the app scales.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It doesn’t have to be this way. Blade files deserve the same contracts and structure we give to APIs, database models, or services. With a few clear practices, you can bring discipline, contracts, and structure to Blade. &lt;/p&gt;
&lt;p&gt;This article will show you the complete approach.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The problem of passing data as arrays or variables in views — solved by introducing &lt;strong&gt;ViewModels&lt;/strong&gt; as contracts.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Autocomplete in Blade&lt;/strong&gt; so your IDE actually helps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Annotation&lt;/strong&gt; to views and partials bound to their own ViewModels.&lt;/li&gt;
&lt;li&gt;Lock down data structures with &lt;strong&gt;DTO classes&lt;/strong&gt; for strict typing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Establish Principles&lt;/strong&gt; for sustainable Blade development.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;&lt;p&gt;Remember, we are only targeting blade for now. Not Twig or Antlr.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Grab a coffee — this is a long read. But by the end, you’ll never look at Blade the same way again.&lt;/p&gt;
&lt;h3 id=&quot;the-crime-scene-what-s-wrong-with-array-soup-in-blade&quot;&gt;The Crime Scene: What’s Wrong With “Array Soup” in Blade&lt;/h3&gt;
&lt;p&gt;Here’s the standard Laravel approach we’ve all seen in controllers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ProductController extends Controller
{
    public function index()
    {
		$products = Product::all();
		$categories = Category::all();
		$brand = Brand::all();
        $title = 'Products';
		
		return view('products.index', compact('products','categories','brands','title') );
    }
}

// Or may be

class ProductController extends Controller
{
    public function index()
    {
        return view('products.index', [
            'title' =&amp;gt; 'Products',
            'products' =&amp;gt; Product::all(),
            'categories' =&amp;gt; Category::all(),
            'brand' =&amp;gt; Brand::all(),	
        ]);
    }
}

// Or a little cleaner approach

class ProductController extends Controller
{
    public function index()
    {
		$data = [
            'title' =&amp;gt; 'Products',
            'products' =&amp;gt; Product::all(),
            'categories' =&amp;gt; Category::all(),
            'brand' =&amp;gt; Brand::all(),	
		];
		
		return view('products.index', $data);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here is how we do it for partials.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pass no data, access product from current blade context inside partials/product.blade.php
@foreach ($products as $product)
    @include('products.partials.product')
@endforeach

// or pass some additional data
@foreach ($products as $product)
    @include('products.partials.product', ['editable' =&amp;gt; $editable])
@endforeach
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally for components.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@foreach ($products as $product)
    &amp;lt;x-product-row :name=&quot;$product-&amp;gt;name&quot; :price =&quot;$product-&amp;gt;price&quot; /&amp;gt;
@endforeach
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-old-lovely-blade&quot;&gt;The Old Lovely Blade&lt;/h3&gt;
&lt;p&gt;Here is how it works well.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{{ $title }}&amp;lt;/h1&amp;gt;

&amp;lt;ul&amp;gt;
    @foreach ($products as $product)
        &amp;lt;li&amp;gt;{{ $product-&amp;gt;name }} ({{ $product-&amp;gt;price }})&amp;lt;/li&amp;gt;
        {{-- or partial --}}
        @include('products.partials.product')        
        {{-- or component --}}
        &amp;lt;x-product-row :name=&quot;$product-&amp;gt;name&quot; :price =&quot;$product-&amp;gt;price&quot; /&amp;gt;
    @endforeach
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works but ask yourself:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Do you actually remember all the variables passed to your views?&lt;/li&gt;
&lt;li&gt;Do you get autocomplete in Blade to help you catch typos?&lt;/li&gt;
&lt;li&gt;What about partials and includes — are you sure the data they rely on is really there?&lt;/li&gt;
&lt;li&gt;If a teammate edits the controller, how do you even know the view won’t silently break?&lt;/li&gt;
&lt;li&gt;How many times have you opened a Blade file and had no clue what’s in scope?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;1-introducing-viewmodels-contracts-for-your-views&quot;&gt;1. Introducing ViewModels: Contracts for Your Views&lt;/h3&gt;
&lt;p&gt;Instead of throwing arrays around, define a ViewModel, a simple &lt;strong&gt;DTO class.&lt;/strong&gt; It declares exactly what data is expected.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\ViewModels;


class ProductsViewModel
{
    public string $title;
    public array $products;
    public array $categories;
    public array $brand;    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And controller.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ProductController extends Controller
{
    public function index()
    {
        $viewModel = new ProductsViewModel();

        $viewModel-&amp;gt;title = 'Products';
        $viewModel-&amp;gt;products = Product::all();
        $viewModel-&amp;gt;categories = Category::all();
        $viewModel-&amp;gt;brands = Brand::all();        

        return view('products.index', ['model' =&amp;gt; $viewModel]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Suggestion:&lt;/strong&gt; &lt;em&gt;Always pass view-model with name &lt;strong&gt;model&lt;/strong&gt; for consistancy. This will help in autocomplete&lt;/em&gt;.  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now blade file will look like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;h1&amp;gt;{{ $model-&amp;gt;title }}&amp;lt;/h1&amp;gt;

&amp;lt;ul&amp;gt;
    @foreach ($model-&amp;gt;products as $product)
        &amp;lt;li&amp;gt;{{ $product-&amp;gt;name }} ({{ $product-&amp;gt;price }})&amp;lt;/li&amp;gt;
        {{-- or partial  --}}
        @include('products.partials.product',['model' =&amp;gt; $product])  
        {{-- or component --}}
        &amp;lt;x-product-row :model=&quot;$product&quot; /&amp;gt;
    @endforeach
&amp;lt;/ul&amp;gt;

@include('partials.brands', ['model' =&amp;gt; $model-&amp;gt;brands])
@include('partials.categories', ['model' =&amp;gt; $model-&amp;gt;categories])
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Suggestion**&lt;/strong&gt;:&lt;strong&gt; For partials and components, always pass data with name &lt;/strong&gt;model** for  consistancy. This will help in autocomplete._&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The contract is explicit.&lt;/li&gt;
&lt;li&gt;Adding or removing data requires updating the ViewModel.&lt;/li&gt;
&lt;li&gt;Each view, partial or component has a single data access point &lt;strong&gt;$model&lt;/strong&gt; to start with.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-installing-extension-for-autocomplete&quot;&gt;2. Installing Extension for Autocomplete&lt;/h3&gt;
&lt;p&gt;This step does not require any code setup but requires installing plugin/extension in your IDE. I use &lt;strong&gt;VS Code&lt;/strong&gt; for Laravel development. I found an excellent extension &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.devsense.com/en&quot;&gt;PHP Tools by DevSense&lt;/a&gt; for VS Code which I used to use in &lt;strong&gt;Visual Studio&lt;/strong&gt; when working with &lt;strong&gt;.Net framework&lt;/strong&gt;. This extension is available for &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursor.com/&quot;&gt;Cursor&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://zed.dev/&quot;&gt;Zed&lt;/a&gt; ,&lt;a rel=&quot;nofollow&quot; href=&quot;https://visualstudio.microsoft.com/&quot;&gt;Visual Studio&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://code.visualstudio.com/Download&quot;&gt;VS Code&lt;/a&gt;. But if you are using a different editor you need to find similar extension.&lt;/p&gt;
&lt;p&gt;Once installed we need to move to next step. Take a look at the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.devsense.com/en/features&quot;&gt;feature list&lt;/a&gt; to find what's available.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When installing PHP Tools I had to ditch Intellisense and Intelliphence but I still get all what they provide with PHP Tools.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-enabling-autocomplete-in-blade&quot;&gt;3. Enabling Autocomplete in Blade&lt;/h3&gt;
&lt;p&gt;Even with ViewModels, Blade doesn’t automatically know the type of &lt;code&gt;`$model&lt;/code&gt;`. The trick is to annotate it in blade views, partials and components:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// views/products/index.blade.php
@php
    /** @var \App\ViewModels\ProductsViewModel $model */
@endphp

&amp;lt;ul&amp;gt;
    @foreach ($model-&amp;gt;products as $product)
        &amp;lt;li&amp;gt;{{ $product-&amp;gt;name }} ({{ $product-&amp;gt;price }})&amp;lt;/li&amp;gt;
        {{-- or partial  --}}
        @include('products.partials.product')  
        {{-- or component --}}
        &amp;lt;x-product-row :name=&quot;$product-&amp;gt;name&quot; :price =&quot;$product-&amp;gt;price&quot; /&amp;gt;
    @endforeach
&amp;lt;/ul&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You need to do same in partial or component. Here is partial.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// views/products/partials/product.blade.php
@php
    /** @var \App\Models\Product $model */
@endphp

&amp;lt;span&amp;gt;
    {{ $model-&amp;gt;name }}
&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, a component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// views/components/product.blade.php
@php
    /** @var \App\View\Components\Product $model */
@endphp

&amp;lt;span&amp;gt;
    {{ $model-&amp;gt;name }}
&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, in VS Code, typing &lt;code&gt;`$model-&amp;gt;`&lt;/code&gt; will bring up real autocomplete suggestions.&lt;/p&gt;
&lt;p&gt;This is a game-changer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No more guessing property names.&lt;/li&gt;
&lt;li&gt;Mistyped variables get caught instantly.&lt;/li&gt;
&lt;li&gt;New developers know exactly what’s available in the view.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;4-enforce-types-at-runtime&quot;&gt;4. Enforce Types at Runtime&lt;/h3&gt;
&lt;p&gt;Since now we are passing ViewModels into views, partials or components and defining annonations in each blade file, lets enforce blade to accept specific type of class. To do this i added two files.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;TypedViewFactory&lt;/strong&gt; – A custom view factory that parses &lt;code&gt;`@var`&lt;/code&gt; declarations at the top of Blade files and enforces them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypedViewServiceProvider&lt;/strong&gt; – Wires everything into Laravel automatically.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Checkout the code &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/raheelshan/typed-blade-views&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; if you declare a type in a Blade file, Laravel must pass the correct class, array, or collection. Otherwise, it throws an exception.&lt;/p&gt;
&lt;p&gt;Declaring Types in Blade makes views predictable and strict.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Main View&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{-- products/index.blade.php --}}
@php /** @var App\ViewModels\ProductsViewModel $model */ @endphp
&amp;lt;h1&amp;gt;{{ $model-&amp;gt;title }}&amp;lt;/h1&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only &lt;code&gt;`ProductsViewModel`&lt;/code&gt; works — anything else throws an exception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partial&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{-- products/partials/product.blade.php --}}
@php 
    /** @var App\Models\Product $model */ 
@endphp
&amp;lt;div&amp;gt;{{ $model-&amp;gt;title }}&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The partial now demands a &lt;code&gt;`Post`&lt;/code&gt; instance, not random data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Component&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// views/components/product.blade.php
@php
    /** @var \App\View\Components\Product $model */
@endphp

&amp;lt;span&amp;gt;
    {{ $model-&amp;gt;name }}
&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The component demands Product class instance now.&lt;/p&gt;
&lt;p&gt;You can do the same with arrays and collection or other primitive types.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// example of array
@php 
    /** @var App\Models\Product[] $model*/ 
@endphp

// Or collection
@php 
    /** @var \Illuminate\Support\Collection&amp;lt;App\Models\Product&amp;gt; $model */ 
@endphp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if wrong type data is passed it will throw the error.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;View [products] expects $model of type App\ViewModels\ProductsViewModel, 
but got string.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similiarly, partials and components will throw the error if passed wrong type of data.&lt;/p&gt;
&lt;h3 id=&quot;5-going-fully-typed-with-dto-classes&quot;&gt;5. Going Fully Typed With DTO Classes&lt;/h3&gt;
&lt;p&gt;So far, we’ve wrapped data in ViewModels, but what about the data itself? Passing raw &lt;strong&gt;Eloquent&lt;/strong&gt; models to views can be risky — &lt;em&gt;they carry too much baggage and autocomplete will present you a jungle of eloquent methods&lt;/em&gt; along with model properties and we will have to stuggle finding properties again . To counter this issue, let's create &lt;strong&gt;DTO Classes&lt;/strong&gt; to describe exactly what's needed. &lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Since &lt;code&gt;component&lt;/code&gt; itself can act as &lt;code&gt;DTO class&lt;/code&gt; and carry specific properties, it does not need additional DTO class.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Lets create a BaseDTO class to define some common methods.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\DTO;

use InvalidArgumentException;

abstract class BaseDTO
{
    public function __construct(array $data = [])
    {
        foreach ($data as $key =&amp;gt; $value) {
            if (!property_exists($this, $key)) {
                throw new InvalidArgumentException(
                    &quot;Property '{$key}' is not defined in &quot; . static::class
                );
            }
            $this-&amp;gt;$key = $value;
        }
    }

    public function __get($name)
    {
        throw new InvalidArgumentException(
            &quot;Tried to access undefined property '{$name}' on &quot; . static::class
        );
    }

    public function __set($name, $value)
    {
        throw new InvalidArgumentException(
            &quot;Tried to set undefined property '{$name}' on &quot; . static::class
        );
    }

    public static function columns(): array
    {
        return array_map(
            fn($prop) =&amp;gt; $prop-&amp;gt;getName(),
            (new \ReflectionClass(static::class))-&amp;gt;getProperties()
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now our actual &lt;strong&gt;SimpleProduct.php&lt;/strong&gt; class to define only specific properties required for blade view or partial.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\DTO\Product;

use App\DTO\BaseDTO;

class SimpleProduct extends BaseDTO
{
    public string $name;
    public string $image;
    public float $price;
    public int $stock;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here's our controller now.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ProductController extends Controller
{
    public function index()
    {
        $viewModel = new ProductsViewModel();

        $viewModel-&amp;gt;products = Product::select(SimpleProduct::columns())-&amp;gt;get();
        // other code      

        return view('products.index', ['model' =&amp;gt; $viewModel]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in partial instead of defining Eloquent model as annotation define DTO class as annotation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// views/products/partials/product.blade.php
@php
    /** @var \App\DTO\Product\SimpleProduct $model */
@endphp

&amp;lt;span&amp;gt;
    {{ $model-&amp;gt;name }}
&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now autcomplete will present you only DTO properties.&lt;/p&gt;
&lt;p&gt;But wait. There's some issue. We passed Eloquent model but annotating DTO class. How's that supposed to work?&lt;/p&gt;
&lt;p&gt;Pay attention on what we selected. Only sepecific columns right. So even though we passed eloquent, due to annotation we will be able to see only DTO properties. But we have to follow one condition.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Suggestion:&lt;/strong&gt; DTO class properties must be same as Eloquent model columns.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Now here are some points to consider when working this approach.&lt;/p&gt;
&lt;h4 id=&quot;strict-enforcement&quot;&gt;Strict Enforcement&lt;/h4&gt;
&lt;p&gt;If you only select &lt;code&gt;`DTO::columns()`&lt;/code&gt; in your query and pass raw models to Blade, IDE autocomplete will still work (thanks to the DTO annotation). But at runtime, Blade can still access any hidden column, relation, or accessor. To enforce discipline beyond autocomplete, wrap results into DTOs.  &lt;/p&gt;
&lt;h4 id=&quot;schema-drift-protection&quot;&gt;Schema Drift Protection&lt;/h4&gt;
&lt;p&gt;If a DTO includes a property that doesn’t exist in the table, MySQL will fail loudly with “Unknown column error”. This is actually a feature — it ensures your DTOs always stay in sync with the schema instead of drifting silently.&lt;/p&gt;
&lt;h4 id=&quot;losing-eloquent-helpers&quot;&gt;Losing Eloquent Helpers&lt;/h4&gt;
&lt;p&gt;DTOs are deliberately &quot;dumb&quot; carriers. Any accessors, relations, or helpers from your Eloquent models are gone. Prepare and transform data in the controller or service layer before converting to DTOs. Views should only see the minimal contract they need.&lt;/p&gt;
&lt;h4 id=&quot;mapping-overhead&quot;&gt;Mapping Overhead&lt;/h4&gt;
&lt;p&gt;Mapping into DTOs isn’t as heavy as it sounds. With a &lt;code&gt;`BaseDTO`&lt;/code&gt; base and a mapper helper, it’s just one extra line in the query pipeline. Think of it as casting: you do it once, and the rest of your view logic becomes predictable and safe.  &lt;/p&gt;
&lt;h4 id=&quot;a-generic-dto-mapper&quot;&gt;A Generic DTO mapper&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;class DTOMapper
{
    public static function map(object $source, string $dtoClass): object
    {
        $dtoReflection = new ReflectionClass($dtoClass);
        $properties = $dtoReflection-&amp;gt;getProperties();

        $args = [];

        foreach ($properties as $property) {
            $name = $property-&amp;gt;getName();

            if (isset($source-&amp;gt;$name)) {
                $args[$name] = $source-&amp;gt;$name;
            }
        }

        return new $dtoClass(...$args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example implementation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// For models
$dto = DTOMapper::map($product, SimpleProduct::class);

// for arrays or collection
$dtos = $products-&amp;gt;map(fn($p) =&amp;gt; DTOMapper::map($p, ProductDTO::class));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;principles-of-structured-blade&quot;&gt;Principles of Structured Blade&lt;/h3&gt;
&lt;p&gt;This approach only holds up if you enforce some ground rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No untyped arrays in views&lt;/strong&gt; Passing raw arrays to Blade is outlawed. Every view should receive a single, typed contract.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One ViewModel per Blade file&lt;/strong&gt; Keep the mental load low. A view gets exactly one entry point: its ViewModel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partials and components need contracts too&lt;/strong&gt; Even the tiniest include deserves its own ViewModel. Contracts everywhere, no freeloaders.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Always annotate with &lt;code&gt;@var&lt;/code&gt;&lt;/strong&gt; Autocomplete is non-negotiable. Every Blade file starts with a &lt;code&gt;`@var`&lt;/code&gt; docblock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Always pass data as model for consistancy&lt;/strong&gt; Every view, partial or component must be passed viewmodel as &lt;code&gt;$model&lt;/code&gt; so that all blade views has single entry point to start with.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business logic stays out of Blade&lt;/strong&gt; Views format and render. Data shaping, conditions, and prep all belong in the ViewModel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use DTOs to avoid Eloquent jungle&lt;/strong&gt; Keep Eloquent in the backend. Views should only see pure, predictable DTOs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict access, fail loudly&lt;/strong&gt; If a property isn’t defined in the ViewModel, the view should throw—no guessing, no silent nulls.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;final-word&quot;&gt;Final Word&lt;/h3&gt;
&lt;p&gt;Blade isn’t broken. The way most developers use it is.&lt;/p&gt;
&lt;p&gt;Treating Blade files as unstructured trash bins creates brittle, unmaintainable code. Adding ViewModels, autocomplete, partial contracts, and DTOs gives your views the same level of professionalism as the rest of your codebase.&lt;/p&gt;
&lt;p&gt;This isn’t overkill. It’s discipline. And discipline is what separates fragile projects from sustainable ones.&lt;/p&gt;
&lt;p&gt;So stop passing arrays. Start passing contracts. Give your Blade files the structure they deserve.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you found this post helpful, consider supporting my work — it means a lot.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://raheelshan.com/payment&quot;&gt;Support my work&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14222/dont-pass-array-or-variables-to-laravel-blade-views-instead-do-this</guid>
<pubDate>Wed, 08 Apr 2026 10:20:36 +0000</pubDate>
</item>
<item>
<title>Introducing The Atomic Query Construction (AQC) Design Pattern</title>
<link>https://coderlegion.com/14221/introducing-the-atomic-query-construction-aqc-design-pattern</link>
<description>&lt;p&gt;When I started my career as a developer, one of my senior team members used to construct queries in core PHP using concatenation based on different parameters. Once the construction was complete, he would echo it for different use cases and run it in PHPMyAdmin or SQLyog. Once satisfied, he’d move on. I always liked the way he built those queries.&lt;/p&gt;
&lt;p&gt;Later, when I moved on to CodeIgniter and eventually Laravel, I noticed something: Developers often write SQL or ORM-based queries buried deep inside controllers, services, views, event listeners — even helpers. This scattered approach leads to redundancy and painful maintenance. We’ve all seen this.&lt;/p&gt;
&lt;p&gt;This always bugged me. Why repeat the same logic scattered across the application when it can be centralized? &lt;/p&gt;
&lt;p&gt;Being an OOP geek, I finally settled on a pattern that works cleanly and elegantly. To counter this problem, I am introducing a new design principle: Atomic Query Construction — or AQC. It’s a design approach where each query lives in its own class. You write it once and use it everywhere. It’s structured, predictable, and aligned with better software design principles.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-aqc-&quot;&gt;What is AQC?&lt;/h3&gt;
&lt;p&gt;AQC stands for Atomic Query Construction. At its core, it’s a principle of isolating each query into its own class. Each of these classes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accepts Parameters&lt;/li&gt;
&lt;li&gt;Constructs Query based on those parameters for different use cases&lt;/li&gt;
&lt;li&gt;Returns Results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You only call this class whenever that specific query is needed — nothing else. It becomes the single source of truth for that operation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;rules-of-the-pattern&quot;&gt;Rules of the Pattern&lt;/h3&gt;
&lt;h5 id=&quot;1-one-class-one-responsibility&quot;&gt;1. One Class, One Responsibility&lt;/h5&gt;
&lt;p&gt;Every class should focus on a single, well-defined query. Stick to the Single Responsibility Principle (SRP).&lt;/p&gt;
&lt;h5 id=&quot;2-accept-parameters&quot;&gt;2. Accept Parameters&lt;/h5&gt;
&lt;p&gt;Each class should accept parameters — filters, identifiers, flags — whatever’s needed to shape the query dynamically.&lt;/p&gt;
&lt;h5 id=&quot;3-construct-the-query-internally&quot;&gt;3. Construct the Query Internally&lt;/h5&gt;
&lt;p&gt;Based on the parameters, the class should build and return a query.&lt;/p&gt;
&lt;h5 id=&quot;4-single-source-of-truth&quot;&gt;4. Single Source of Truth&lt;/h5&gt;
&lt;p&gt;Anywhere in the app where you need this data — whether in a controller, event, or service — call this class. Do NOT rewrite the query logic elsewhere.&lt;/p&gt;
&lt;h5 id=&quot;5-handle-method&quot;&gt;5. Handle Method&lt;/h5&gt;
&lt;p&gt;Each class should expose a single public method, handle(). It can have private helpers internally, but the interface stays clean.&lt;/p&gt;
&lt;h5 id=&quot;6-consistent-naming&quot;&gt;6. Consistent Naming&lt;/h5&gt;
&lt;p&gt;Class names must be clear and consistent. For a resource like &lt;code&gt;Product&lt;/code&gt;, your AQC classes might be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GetAllProducts&lt;/li&gt;
&lt;li&gt;GetProduct&lt;/li&gt;
&lt;li&gt;StoreProduct&lt;/li&gt;
&lt;li&gt;UpdateProduct&lt;/li&gt;
&lt;li&gt;DeleteProduct&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;benefits&quot;&gt;Benefits&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Modular&lt;/strong&gt;: Every query is isolated and self-contained. You know exactly where to look when changes are needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusable&lt;/strong&gt;: Once written, the query class can be reused across the app with different params or use cases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible&lt;/strong&gt;: It supports multiple scenarios (admin view, frontend listing, etc.) using dynamic input.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clean Separation&lt;/strong&gt;: Keeps your controllers, services, and views clean. No query logic there — just calls to your AQC classes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testable&lt;/strong&gt;: Because logic is in isolated classes, it’s easy to write unit tests without involving controllers or database state.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;implementation-example&quot;&gt;Implementation Example&lt;/h3&gt;
&lt;p&gt;Let’s say we’re working with a &lt;code&gt;Product&lt;/code&gt; model in a typical eCommerce app. First, create a folder:  &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app/AQC/Product/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetAllProducts.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetProduct.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;StoreProduct.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;UpdateProduct.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DeleteProduct.php&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;“While I’ll use Laravel in the code examples, this pattern is framework-agnostic and can be adapted to any OOP-based backend environment — Node.js, PHP, Python, etc.”&lt;/p&gt;
&lt;p&gt;GetAllProducts.php&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\AQC\Product;

use App\Models\Product;

class GetAllProducts
{
    public static function handle($params = [], $paginate = true, $scenario = 'default')
    {
        $productObj = Product::latest('id');

        if (isset($params['category_id']) &amp;amp;&amp;amp; $params['category_id'] &amp;gt; 0) {
            $productObj-&amp;gt;where('category_id', $params['category_id']);
        }

        if (isset($params['brand_id']) &amp;amp;&amp;amp; $params['brand_id'] &amp;gt; 0) {
            $productObj-&amp;gt;where('brand_id', $params['brand_id']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj-&amp;gt;select(['id', 'name']);
                break;
            case 'compact':
                $productObj-&amp;gt;select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj-&amp;gt;select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj-&amp;gt;select('*');
        }

        return $paginate
            ? $productObj-&amp;gt;paginate(Product::PAGINATE)
            : $productObj-&amp;gt;get();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:  &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Admin listing
$products = GetAllProducts::handle(); 
// return paginated product order id latest 

// Frontend product listing
$products = GetAllProducts::handle($request-&amp;gt;all(), true, 'compact');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;GetProduct.php&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\AQC\Product;
use App\Models\Product;

class GetProduct
{
    public static function handle($id, $params = [], $scenario = 'default')
    {
        $productObj = Product::where('id', $id);

        if (isset($params['active']) &amp;amp;&amp;amp; $params['active']) {
            $productObj-&amp;gt;where('active', $params['active']);
        }

        // add more conditions for different use cases

        switch ($scenario) {
            case 'minimal':
                $productObj-&amp;gt;select(['id', 'name']);
                break;
            case 'compact':
                $productObj-&amp;gt;select(['id', 'name', 'price', 'image']);
                break;
            case 'admin':
                $productObj-&amp;gt;select(['id', 'name', 'price', 'sku', 'image', 'stock', 'cost']);
                break;
            default:
                $productObj-&amp;gt;select('*');
        }

        return $productObj-&amp;gt;first();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Admin edit screen
$product = GetProduct::handle($productID, [], 'admin');

// Frontend product detail
$product = GetProduct::handle($productID, [], 'compact');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;StoreProduct.php&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\AQC\Product;
use App\Models\Product;

class StoreProduct
{
    public static function handle($params)
    {
        $product = new Product();
        $product-&amp;gt;fill($params);

        if (isset($params['type']) &amp;amp;&amp;amp; $params['type'] === 'combo') {
            $product-&amp;gt;is_combo = true;
        }

        if (!isset($params['sku'])) {
            $product-&amp;gt;sku = self::generateSku($params);
        }

        $product-&amp;gt;save();
        return $product;
    }

    private static function generateSku($params)
    {
        // SKU generation logic
        return 'SKU-' . rand(1000, 9999);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UpdateProduct.php&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\AQC\Product;
use App\Models\Product;

class UpdateProduct
{
    public static function handle($id, $params)
    {
        $product = Product::find($id);

        if (!$product) return null;

        if (isset($params['cost'])) $product-&amp;gt;cost = $params['cost'];
        if (isset($params['price'])) $product-&amp;gt;price = $params['price'];
        if (isset($params['stock'])) $product-&amp;gt;stock = $params['stock'];

        // add more fields as needed

        $product-&amp;gt;save();
        return $product;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DeleteProduct.php&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php

namespace App\AQC\Product;
use App\Models\Product;

class DeleteProduct
{
    public static function handle($params = [])
    {
        $productsObj = Product::query();

        if (!empty($params['category_id'])) {
            $productsObj-&amp;gt;where('category_id', $params['category_id']);
        }

        if (!empty($params['brand_id'])) {
            $productsObj-&amp;gt;where('brand_id', $params['brand_id']);
        }

        if (!empty($params['product_id'])) {
            $productsObj-&amp;gt;where('id', $params['product_id']);
        }

        if (!$product) return false;

        $productObj-&amp;gt;delete();

        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Each class builds the query, covers various scenarios, and cleanly separates logic. You can now reuse queries across your entire app — controllers, API endpoints, and admin panels — without rewriting anything.&lt;/p&gt;
&lt;p&gt;When a new requirement comes in, you know exactly where to go. No more chasing query logic across 20 files.&lt;/p&gt;
&lt;p&gt;“With AQC, every query has a home. No more hunting across layers of your app to find and fix logic. Write it once. Use it everywhere. Welcome to clean, atomic thinking.”&lt;/p&gt;
&lt;p&gt;If you try AQC in your projects, I’d love to hear how it works for you — or what you’d improve.&lt;/p&gt;
&lt;p&gt;I’ll be sharing more Laravel architecture patterns soon. Follow along if you’re into clean, scalable code.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you’d like personal mentorship or 1-on-1 help with &lt;code&gt;AQC Design Pattern&lt;/code&gt;, I’m available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://topmate.io&quot;&gt;Topmate.io&lt;/a&gt; here.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://topmate.io/raheelshan/&quot;&gt;Schedule a booking&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you found this post helpful, consider supporting my work — it means a lot.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://raheelshan.com/payment&quot;&gt;Support My Work&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14221/introducing-the-atomic-query-construction-aqc-design-pattern</guid>
<pubDate>Wed, 08 Apr 2026 10:17:35 +0000</pubDate>
</item>
<item>
<title>Scaling Laravel Backends for High-Volume Systems: Lessons from Serving 100,000+ Users in Production</title>
<link>https://coderlegion.com/14147/scaling-laravel-backends-for-high-volume-systems-lessons-from-serving-users-production</link>
<description>&lt;h3 id=&quot;how-i-reduced-api-latency-by-88-achieved-99-8-uptime-and-scaled-a-platform-from-2-000-to-15-000-concurrent-users-without-downtime-&quot;&gt;How I reduced API latency by 88%, achieved 99.8% uptime, and scaled a platform from 2,000 to 15,000 concurrent users — without downtime.&lt;/h3&gt;
&lt;p&gt;There is a moment every backend engineer dreads. Traffic is climbing, the monitoring dashboard is turning red, and your database is buckling under the weight of queries it was never designed to handle at this scale. I have lived through that moment more than once.&lt;/p&gt;
&lt;p&gt;Over the past nine years, I, Olamilekan Lamidi, have built and scaled backend systems across fintech, gaming, health-tech, and logistics — platforms that collectively serve tens of thousands of users daily. This article distils the hard-earned lessons from scaling Laravel backends to handle high-volume traffic, reduce API latency, and maintain near-perfect uptime under pressure.&lt;/p&gt;
&lt;p&gt;If you are building a Laravel application that needs to graduate from &quot;it works&quot; to &quot;it works at scale,&quot; this is for you.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-problem-when-your-backend-becomes-the-bottleneck&quot;&gt;The Problem: When Your Backend Becomes the Bottleneck&lt;/h2&gt;
&lt;p&gt;Most Laravel applications start life the same way: a clean MVC structure, Eloquent models that read like poetry, and Blade templates that ship fast. This works beautifully at 1,000 users. At 10,000, cracks appear. At 100,000+, the whole thing can collapse.&lt;/p&gt;
&lt;p&gt;I encountered this problem in its most acute form while leading backend engineering at &lt;strong&gt;Lordwin Group&lt;/strong&gt;, where I architected systems for an investment and hotel management platform serving over 10,000 registered users, alongside a real-time gaming backend supporting 3,000+ concurrent connections. The platform was haemorrhaging performance. API response times had crept above two seconds, database queries were timing out during peak hours, and the engineering team was firefighting daily.&lt;/p&gt;
&lt;p&gt;The root causes were familiar to anyone who has scaled a Laravel application:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;N+1 query explosions&lt;/strong&gt; buried deep in nested Eloquent relationships&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unindexed database tables&lt;/strong&gt; that grew silently from thousands to millions of rows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monolithic request handling&lt;/strong&gt; where every API call did too much synchronous work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No caching strategy&lt;/strong&gt; — every request hit the database, every time&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session and queue bottlenecks&lt;/strong&gt; running on the same infrastructure as the application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The question was not whether the system needed re-architecting. It was whether we could do it without taking the platform offline.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-challenges-scaling-without-breaking&quot;&gt;The Challenges: Scaling Without Breaking&lt;/h2&gt;
&lt;p&gt;Scaling a live production system is fundamentally different from building a new one. You cannot afford downtime. You cannot rewrite everything at once. And you have to keep shipping features while simultaneously rebuilding the engine underneath.&lt;/p&gt;
&lt;p&gt;Here are the specific challenges I faced:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Database Performance Degradation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The MySQL database had grown to contain tables with 5+ million rows. Queries that once returned in milliseconds were now taking 3–8 seconds. Eloquent's lazy loading was making things worse — a single API endpoint for listing investment portfolios was firing 47 database queries per request.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Synchronous Processing Overload&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Critical operations — sending transactional emails, generating PDF reports, processing payment webhooks — were all running synchronously within the HTTP request cycle. This meant users were waiting 4–6 seconds for operations that did not need to block the response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Absence of a Caching Layer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There was no Redis or Memcached layer. Configuration data, user permissions, and even static reference data were fetched from the database on every single request. During peak traffic, the database connection pool was exhausted within minutes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Infrastructure That Could Not Scale Horizontally&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The application was deployed on a single server with no load balancing, no auto-scaling, and no separation between the application server, queue workers, and database. Everything competed for the same CPU and memory.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-solution-a-systematic-approach-to-laravel-performance-at-scale&quot;&gt;The Solution: A Systematic Approach to Laravel Performance at Scale&lt;/h2&gt;
&lt;p&gt;I approached the problem methodically, treating it as four distinct engineering initiatives that could be executed incrementally without disrupting the live platform.&lt;/p&gt;
&lt;h3 id=&quot;phase-1-query-optimisation-and-database-restructuring&quot;&gt;Phase 1: Query Optimisation and Database Restructuring&lt;/h3&gt;
&lt;p&gt;The single highest-impact change was eliminating N+1 queries and restructuring how the application interacted with the database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Eager Loading Enforcement&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I audited every Eloquent query across 15+ modules using Laravel Debugbar and Clockwork, cataloguing every N+1 violation. I then introduced eager loading constraints:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Before: 47 queries for a portfolio listing
$portfolios = Portfolio::all();
foreach ($portfolios as $portfolio) {
    echo $portfolio-&amp;gt;user-&amp;gt;name;
    echo $portfolio-&amp;gt;transactions-&amp;gt;count();
}

// After: 3 queries with eager loading and aggregation
$portfolios = Portfolio::with(['user:id,name', 'transactions'])
    -&amp;gt;withCount('transactions')
    -&amp;gt;paginate(50);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Strategic Indexing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I analysed slow query logs and added composite indexes on columns used in &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt;, and &lt;code&gt;ORDER BY&lt;/code&gt; clauses. For the investment transactions table alone, adding a composite index on &lt;code&gt;(user_id, status, created_at)&lt;/code&gt; reduced a critical query from 6.2 seconds to 180 milliseconds.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE INDEX idx_transactions_user_status_date
ON transactions (user_id, status, created_at);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Query Refactoring&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For complex reporting queries, I replaced Eloquent with raw SQL and database views where the ORM was generating inefficient joins. I also introduced read replicas to offload reporting queries from the primary database.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Average query execution time dropped by 60%. The portfolio listing endpoint went from 2.3 seconds to 320 milliseconds.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-2-asynchronous-processing-with-queue-architecture&quot;&gt;Phase 2: Asynchronous Processing with Queue Architecture&lt;/h3&gt;
&lt;p&gt;I decoupled all non-critical work from the HTTP request cycle using Laravel's queue system backed by Redis.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class ProcessPaymentWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;

    public function handle(): void
    {
        $this-&amp;gt;updateTransactionStatus();
        $this-&amp;gt;notifyUser();
        $this-&amp;gt;syncWithAccountingService();
        $this-&amp;gt;generateReceipt();
    }

    public function failed(Throwable $exception): void
    {
        Log::critical('Payment webhook failed', [
            'transaction_id' =&amp;gt; $this-&amp;gt;transaction-&amp;gt;id,
            'error' =&amp;gt; $exception-&amp;gt;getMessage(),
        ]);
        AlertService::notifyEngineering($exception);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I configured dedicated queue workers for different job priorities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High priority:&lt;/strong&gt; Payment processing, authentication events&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Medium priority:&lt;/strong&gt; Email notifications, PDF generation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low priority:&lt;/strong&gt; Analytics aggregation, cache warming&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; API response times for webhook endpoints dropped from 4.2 seconds to under 200 milliseconds. The user-facing experience became near-instantaneous.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-3-multi-layer-caching-strategy&quot;&gt;Phase 3: Multi-Layer Caching Strategy&lt;/h3&gt;
&lt;p&gt;I implemented a three-tier caching architecture using Redis:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Application-Level Caching&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;class PermissionService
{
    public function getUserPermissions(int $userId): Collection
    {
        return Cache::tags([&quot;user:{$userId}&quot;, 'permissions'])
            -&amp;gt;remember(
                &quot;user_permissions:{$userId}&quot;,
                now()-&amp;gt;addHours(6),
                fn () =&amp;gt; $this-&amp;gt;repository-&amp;gt;getPermissionsForUser($userId)
            );
    }

    public function invalidateUserCache(int $userId): void
    {
        Cache::tags([&quot;user:{$userId}&quot;])-&amp;gt;flush();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Route-Level Caching&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For endpoints serving semi-static data (e.g., hotel listings, investment product catalogues), I introduced HTTP response caching with cache invalidation tied to model events:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::middleware('cache.response:300')-&amp;gt;group(function () {
    Route::get('/products', [ProductController::class, 'index']);
    Route::get('/listings', [ListingController::class, 'index']);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Database Query Caching&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For expensive aggregation queries used in dashboards, I implemented scheduled cache warming through Laravel's task scheduler, ensuring cached data was always fresh without burdening the database during peak hours.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Database load reduced by approximately 70% during peak traffic. Redis handled 85% of read requests, and the database connection pool was no longer a bottleneck.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;phase-4-infrastructure-and-deployment-architecture&quot;&gt;Phase 4: Infrastructure and Deployment Architecture&lt;/h3&gt;
&lt;p&gt;I redesigned the deployment topology to support horizontal scaling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Load Balancer (Nginx)&lt;/strong&gt; — Distributed traffic across multiple application instances&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Separated Queue Workers&lt;/strong&gt; — Dedicated servers for background job processing, preventing queue backlogs from impacting API response times&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read Replicas&lt;/strong&gt; — Reporting and analytics queries routed to MySQL read replicas&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker Containerisation&lt;/strong&gt; — Standardised environments across development, staging, and production, eliminating &quot;works on my machine&quot; deployment failures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD Pipeline&lt;/strong&gt; — Automated testing and zero-downtime deployments using rolling updates&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  app:
    build: .
    deploy:
      replicas: 3
    depends_on:
      - mysql
      - redis

  queue-worker:
    build: .
    command: php artisan queue:work redis --queue=high,medium,low
    deploy:
      replicas: 2

  scheduler:
    build: .
    command: php artisan schedule:run

  mysql:
    image: mysql:8.0
    volumes:
      - db_data:/var/lib/mysql

  redis:
    image: redis:alpine
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; The platform could now scale horizontally by adding application instances. Deployments went from 15-minute maintenance windows to zero-downtime rolling updates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-results-measurable-impact&quot;&gt;The Results: Measurable Impact&lt;/h2&gt;
&lt;p&gt;After executing these four phases over a 10-week period, the performance transformation was significant:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Average API response time&lt;/strong&gt;&lt;br&gt;
Before: 2.3s → After: 280ms → &lt;strong&gt;88% reduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database query execution time&lt;/strong&gt;&lt;br&gt;
Before: 1.8s avg → After: 180ms avg → &lt;strong&gt;60% reduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Payment webhook processing&lt;/strong&gt;&lt;br&gt;
Before: 4.2s → After: 190ms → &lt;strong&gt;95% reduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Database connections at peak&lt;/strong&gt;&lt;br&gt;
Before: 95% utilised → After: 30% utilised → &lt;strong&gt;70% reduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;System uptime (monthly)&lt;/strong&gt;&lt;br&gt;
Before: 96.2% → After: 99.8% → &lt;strong&gt;3.6% improvement&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concurrent user capacity&lt;/strong&gt;&lt;br&gt;
Before: ~2,000 → After: ~15,000 → &lt;strong&gt;7.5x increase&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;These were not theoretical benchmarks. They were measured in production using New Relic APM and custom Prometheus dashboards over a 30-day observation period after the final deployment.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;broader-impact-patterns-that-transfer-across-industries&quot;&gt;Broader Impact: Patterns That Transfer Across Industries&lt;/h2&gt;
&lt;p&gt;The architectural patterns I applied are not unique to any single domain. I have since applied the same principles across:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fintech&lt;/strong&gt; — At 2am Tech, I contributed to Addio, a financial platform used by 500+ organisations, where queue-based architecture and database optimisation were essential for handling high-volume transactional workflows across factors, debtors, and vendors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Logistics&lt;/strong&gt; — At Viaduct, I engineered a high-performance on-demand delivery platform with Laravel and Vue.js serving 5,000+ active users, where real-time order tracking demanded low-latency APIs and efficient WebSocket communication.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enterprise SaaS&lt;/strong&gt; — At VacancySoft, I led the refactoring of a legacy codebase across 15+ modules, migrating services to a modern architecture — cutting query execution time by 60% and improving overall system performance by 40% while handling 50,000+ daily requests.&lt;/p&gt;
&lt;p&gt;The fundamental lesson is this: &lt;strong&gt;scaling is not about throwing more hardware at the problem. It is about understanding where your system spends time and eliminating waste systematically.&lt;/strong&gt; Database queries, synchronous processing, missing caches, and monolithic deployment topologies are the four horsemen of backend performance degradation. Address them methodically, measure relentlessly, and you can scale a Laravel application to handle traffic volumes that would surprise most engineers.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;key-takeaways-for-engineers-building-high-volume-systems&quot;&gt;Key Takeaways for Engineers Building High-Volume Systems&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Profile before you optimise.&lt;/strong&gt; Use Laravel Debugbar, Clockwork, or APM tools to identify actual bottlenecks. Gut instinct is unreliable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eliminate N+1 queries aggressively.&lt;/strong&gt; This is almost always the single highest-impact optimisation in any Eloquent-based application.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Move everything non-essential off the request cycle.&lt;/strong&gt; If the user does not need to wait for it, it belongs in a queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache strategically, invalidate precisely.&lt;/strong&gt; A poorly invalidated cache is worse than no cache. Use tagged caching and model event listeners to keep data fresh.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for horizontal scaling from day one.&lt;/strong&gt; Stateless application servers, externalised sessions (Redis), and containerised deployments make scaling a configuration change rather than an architectural rewrite.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure everything.&lt;/strong&gt; If you cannot quantify the improvement, you cannot prove it happened. Set up monitoring before you start optimising.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Backend performance at scale is a discipline, not a one-time fix. Every system I have worked on has taught me that the architecture decisions you make early compound over time — for better or worse. The patterns described in this article have been tested in production across financial platforms, real-time gaming systems, logistics networks, and enterprise SaaS products.&lt;/p&gt;
&lt;p&gt;If you are an engineer facing similar scaling challenges, I hope this gives you a practical roadmap. And if you have solved these problems differently, I would genuinely like to hear about it. The best engineering solutions come from sharing what we have learned.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14147/scaling-laravel-backends-for-high-volume-systems-lessons-from-serving-users-production</guid>
<pubDate>Tue, 07 Apr 2026 11:30:59 +0000</pubDate>
</item>
<item>
<title>How to Implement Caching in Laravel Using Different Caching Tools (A Practical Guide)</title>
<link>https://coderlegion.com/13765/how-to-implement-caching-in-laravel-using-different-caching-tools-a-practical-guide</link>
<description>&lt;p&gt;If your Laravel application feels slow under load, chances are you're doing too much work on every request.&lt;/p&gt;
&lt;p&gt;Database queries, API calls, heavy computations these things add up fast. And when traffic increases, performance drops, users complain, and suddenly you're firefighting.&lt;/p&gt;
&lt;p&gt;I’ve been there.&lt;/p&gt;
&lt;p&gt;The turning point? &lt;strong&gt;Caching.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When implemented correctly, caching can dramatically improve performance, reduce server load, and make your app feel &lt;em&gt;instant&lt;/em&gt;. In this guide, I’ll walk you through &lt;strong&gt;how to implement caching in Laravel using different caching tools&lt;/strong&gt;, with real world context not just theory.&lt;/p&gt;
&lt;h3 id=&quot;why-caching-matters-in-laravel-applications&quot;&gt;Why Caching Matters in Laravel Applications&lt;/h3&gt;
&lt;p&gt;Before we dive into code, let’s make it real.&lt;/p&gt;
&lt;p&gt;Every time a user hits your endpoint, your app might:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query the database&lt;/li&gt;
&lt;li&gt;Process business logic&lt;/li&gt;
&lt;li&gt;Format a response&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now imagine doing that &lt;strong&gt;thousands of times per minute&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Caching allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store the result once&lt;/li&gt;
&lt;li&gt;Reuse it multiple times&lt;/li&gt;
&lt;li&gt;Avoid unnecessary computation&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Result:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Faster responses, lower costs, happier users.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;understanding-laravel-s-cache-system&quot;&gt;Understanding Laravel’s Cache System&lt;/h3&gt;
&lt;p&gt;Laravel makes caching incredibly simple through its unified API.&lt;/p&gt;
&lt;p&gt;You interact with caching using:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Illuminate\Support\Facades\Cache;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behind the scenes, Laravel supports multiple drivers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Memcached&lt;/li&gt;
&lt;li&gt;Array (for testing)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;The beauty is:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;You can switch drivers without changing your core logic..&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;step-1-basic-caching-in-laravel-start-here-&quot;&gt;Step 1: Basic Caching in Laravel (Start Here)&lt;/h3&gt;
&lt;p&gt;Let’s begin with the simplest use case.&lt;/p&gt;
&lt;h4 id=&quot;storing-data-in-cache&quot;&gt;Storing Data in Cache&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Cache::put('users', $users, 60); // cache for 60 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;retrieving-data&quot;&gt;Retrieving Data&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$users = Cache::get('users');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-better-approach-remember-pattern&quot;&gt;The Better Approach: Remember Pattern&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$users = Cache::remember('users', 60, function () {
    return User::all();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is my go to pattern.&lt;/p&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;It checks the cache first, and only hits the database if needed.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;step-2-choosing-the-right-cache-driver&quot;&gt;Step 2: Choosing the Right Cache Driver&lt;/h3&gt;
&lt;p&gt;Not all caching tools are equal. Your choice depends on your scale and architecture.&lt;/p&gt;
&lt;h4 id=&quot;1-file-cache-default-&quot;&gt;1. File Cache (Default)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Small apps, local development&lt;br&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; &lt;code&gt;storage/framework/cache&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;CACHE_DRIVER=file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Easy to set up&lt;br&gt;
❌ Slow for high traffic&lt;/p&gt;
&lt;h4 id=&quot;2-database-cache&quot;&gt;2. Database Cache&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Shared environments without Redis&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan cache:table
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;CACHE_DRIVER=database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Persistent&lt;br&gt;
❌ Slower than in memory caching&lt;/p&gt;
&lt;h4 id=&quot;3-redis-recommended-for-production-&quot;&gt;3. Redis (Recommended for Production)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; High performance applications&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;CACHE_DRIVER=redis
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require predis/predis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Extremely fast (in memory)&lt;br&gt;
✔ Supports advanced features (queues, pub/sub)&lt;br&gt;
✔ Scales well&lt;/p&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;For Me:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;This is what I use in most production systems.&lt;/span&gt;
&lt;/div&gt;
&lt;h4 id=&quot;4-memcached&quot;&gt;4. Memcached&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Distributed systems&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;CACHE_DRIVER=memcached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Fast and lightweight&lt;br&gt;
❌ Less flexible than Redis&lt;/p&gt;
&lt;h3 id=&quot;step-3-caching-database-queries-real-impact-&quot;&gt;Step 3: Caching Database Queries (Real Impact)&lt;/h3&gt;
&lt;p&gt;This is where you’ll see immediate performance gains.&lt;/p&gt;
&lt;h4 id=&quot;without-caching-&quot;&gt;Without Caching:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$products = Product::where('active', 1)-&amp;gt;get();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;with-caching-&quot;&gt;With Caching:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$products = Cache::remember('active_products', 300, function () {
    return Product::where('active', 1)-&amp;gt;get();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;You just reduced database load significantly.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;step-4-cache-invalidation-the-part-everyone-gets-wrong-&quot;&gt;Step 4: Cache Invalidation (The Part Everyone Gets Wrong)&lt;/h3&gt;
&lt;p&gt;Caching is easy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Invalidation is hard.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you don’t clear or update cache properly, users will see stale data.&lt;/p&gt;
&lt;h4 id=&quot;manual-invalidation-&quot;&gt;Manual Invalidation:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Cache::forget('active_products');
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;example-after-updating-data-&quot;&gt;Example (After Updating Data):&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Product::create($data);
Cache::forget('active_products');
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Always tie cache invalidation to data changes.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;step-5-using-cache-tags-advanced-control-&quot;&gt;Step 5: Using Cache Tags (Advanced Control)&lt;/h3&gt;
&lt;p&gt;If you're using Redis or Memcached, Laravel supports &lt;strong&gt;cache tags&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Cache::tags(['products'])-&amp;gt;put('active_products', $products, 300);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;clearing-by-tag-&quot;&gt;Clearing by Tag:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Cache::tags(['products'])-&amp;gt;flush();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is powerful when dealing with grouped data.&lt;/p&gt;
&lt;h3 id=&quot;step-6-caching-api-responses&quot;&gt;Step 6: Caching API Responses&lt;/h3&gt;
&lt;p&gt;If your app consumes external APIs, caching is a lifesaver.&lt;/p&gt;
&lt;h4 id=&quot;example-&quot;&gt;Example:&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$response = Cache::remember('weather_data', 600, function () {
    return Http::get('https://api.weather.com/data')-&amp;gt;json();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Reduces API calls&lt;br&gt;
✔ Avoids rate limits&lt;br&gt;
✔ Improves response time&lt;/p&gt;
&lt;h3 id=&quot;step-7-full-page-caching-when-you-need-speed-&quot;&gt;Step 7: Full Page Caching (When You Need Speed)&lt;/h3&gt;
&lt;p&gt;For content heavy apps (blogs, dashboards), full page caching can be huge.&lt;/p&gt;
&lt;p&gt;You can use packages like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;spatie/laravel-responsecache&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;This caches entire HTTP responses.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;step-8-cache-configuration-best-practices&quot;&gt;Step 8: Cache Configuration Best Practices&lt;/h3&gt;
&lt;p&gt;Here’s what I’ve learned from production systems:&lt;/p&gt;
&lt;h4 id=&quot;1-use-redis-in-production&quot;&gt;1. Use Redis in Production&lt;/h4&gt;
&lt;p&gt;It’s faster and more reliable.&lt;/p&gt;
&lt;h4 id=&quot;2-set-meaningful-expiry-times&quot;&gt;2. Set Meaningful Expiry Times&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Short lived data → 1–5 minutes&lt;/li&gt;
&lt;li&gt;Semi static data → 10–60 minutes&lt;/li&gt;
&lt;li&gt;Rarely changing → hours or days&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-avoid-cache-stampede&quot;&gt;3. Avoid Cache Stampede&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;remember&lt;/code&gt; to prevent multiple requests hitting the DB at once.&lt;/p&gt;
&lt;h4 id=&quot;4-monitor-cache-usage&quot;&gt;4. Monitor Cache Usage&lt;/h4&gt;
&lt;p&gt;Track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hit rate&lt;/li&gt;
&lt;li&gt;Miss rate&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step-9-common-mistakes-to-avoid&quot;&gt;Step 9: Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Let me save you some pain:&lt;/p&gt;
&lt;p&gt;❌ Caching everything blindly&lt;br&gt;
❌ Forgetting to invalidate cache&lt;br&gt;
❌ Using file cache in production&lt;br&gt;
❌ Storing sensitive data in cache&lt;br&gt;
❌ Setting no expiration&lt;/p&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Caching is powerful but only when used intentionally.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;a-practical-workflow-i-use&quot;&gt;A Practical Workflow I Use&lt;/h3&gt;
&lt;p&gt;When optimizing a Laravel app:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify slow queries/endpoints&lt;/li&gt;
&lt;li&gt;Add caching using &lt;code&gt;Cache::remember()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Choose Redis as driver&lt;/li&gt;
&lt;li&gt;Add invalidation logic&lt;/li&gt;
&lt;li&gt;Monitor performance improvements&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Simple. Effective.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-caching-is-a-performance-multiplier&quot;&gt;Final Thoughts: Caching Is a Performance Multiplier&lt;/h3&gt;
&lt;p&gt;If you’re not using caching in your Laravel app, you’re leaving performance on the table.&lt;/p&gt;
&lt;p&gt;The goal isn’t just speed it’s &lt;strong&gt;scalability and efficiency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A well cached application:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handles more users&lt;/li&gt;
&lt;li&gt;Reduces infrastructure costs&lt;/li&gt;
&lt;li&gt;Delivers a better user experience&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;call-to-action&quot;&gt;Call to Action&lt;/h3&gt;
&lt;p&gt;If this guide helped you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Share it with your Laravel team&lt;/li&gt;
&lt;li&gt;Bookmark it for your next optimization sprint&lt;/li&gt;
&lt;li&gt;Drop a comment: &lt;em&gt;What’s the biggest performance issue you’ve faced in Laravel?&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you’re serious about building scalable systems, start treating caching not as an optimization but as a &lt;strong&gt;core part of your architecture&lt;/strong&gt;.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/13765/how-to-implement-caching-in-laravel-using-different-caching-tools-a-practical-guide</guid>
<pubDate>Tue, 31 Mar 2026 11:12:00 +0000</pubDate>
</item>
<item>
<title>Laravel Testing Environment Setup</title>
<link>https://coderlegion.com/13653/laravel-testing-environment-setup</link>
<description>&lt;hr&gt;
&lt;p&gt;title:  Laravel Testing Environment Setup&lt;br&gt;
published: true&lt;br&gt;
description: &lt;br&gt;
tags: #test #laravel #phpunit&lt;/p&gt;
&lt;h2 id=&quot;published-at-2026-03-26-14-38-0000&quot;&gt;published_at: 2026-03-26 14:38 +0000&lt;/h2&gt;
&lt;hr&gt;
&lt;p&gt;This guide explains how to properly configure a testing environment in Laravel using .env.testing and phpunit.xml.&lt;/p&gt;
&lt;p&gt; Overview&lt;/p&gt;
&lt;p&gt;When running automated tests, you should isolate your testing environment from development and production. This prevents accidental data loss and improves test reliability.&lt;/p&gt;
&lt;p&gt;⚙️ 1. Create .env.testing&lt;/p&gt;
&lt;p&gt;Create a .env.testing file in the root of your Laravel project:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-conf&quot;&gt;APP_NAME=laravel
APP_ENV=local
APP_KEY= # application key
APP_DEBUG=true
APP_URL=http://laravel.test

APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

APP_MAINTENANCE_DRIVER=file

BCRYPT_ROUNDS=12

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE= # your testing database (e.g., laravel-testing)
DB_USERNAME=root
DB_PASSWORD=

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=true
SESSION_PATH=/
SESSION_DOMAIN=null

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Notes&lt;br&gt;
Use a separate database for testing.&lt;br&gt;
Never reuse your development or production database.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure phpunit.xml&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Update your phpunit.xml file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-xml&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;phpunit xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
         xsi:noNamespaceSchemaLocation=&quot;vendor/phpunit/phpunit/phpunit.xsd&quot;
         bootstrap=&quot;vendor/autoload.php&quot;
         colors=&quot;true&quot;
&amp;gt;
    &amp;lt;testsuites&amp;gt;
        &amp;lt;testsuite name=&quot;Unit&quot;&amp;gt;
            &amp;lt;directory&amp;gt;tests/Unit&amp;lt;/directory&amp;gt;
        &amp;lt;/testsuite&amp;gt;
        &amp;lt;testsuite name=&quot;Feature&quot;&amp;gt;
            &amp;lt;directory&amp;gt;tests/Feature&amp;lt;/directory&amp;gt;
        &amp;lt;/testsuite&amp;gt;
    &amp;lt;/testsuites&amp;gt;

    &amp;lt;source&amp;gt;
        &amp;lt;include&amp;gt;
            &amp;lt;directory&amp;gt;app&amp;lt;/directory&amp;gt;
        &amp;lt;/include&amp;gt;
    &amp;lt;/source&amp;gt;

    &amp;lt;php&amp;gt;
        &amp;lt;env name=&quot;APP_ENV&quot; value=&quot;testing&quot;/&amp;gt;
        &amp;lt;env name=&quot;APP_MAINTENANCE_DRIVER&quot; value=&quot;file&quot;/&amp;gt;
        &amp;lt;env name=&quot;BCRYPT_ROUNDS&quot; value=&quot;4&quot;/&amp;gt;

        &amp;lt;env name=&quot;BROADCAST_CONNECTION&quot; value=&quot;null&quot;/&amp;gt;
        &amp;lt;env name=&quot;CACHE_STORE&quot; value=&quot;array&quot;/&amp;gt;
        &amp;lt;env name=&quot;DB_CONNECTION&quot; value=&quot;mysql&quot;/&amp;gt;
        &amp;lt;!-- &amp;lt;env name=&quot;DB_DATABASE&quot; value=&quot;your-testing-db&quot;/&amp;gt; --&amp;gt;
        &amp;lt;env name=&quot;DB_URL&quot; value=&quot;&quot;/&amp;gt;

        &amp;lt;env name=&quot;MAIL_MAILER&quot; value=&quot;array&quot;/&amp;gt;
        &amp;lt;env name=&quot;QUEUE_CONNECTION&quot; value=&quot;sync&quot;/&amp;gt;
        &amp;lt;env name=&quot;SESSION_DRIVER&quot; value=&quot;array&quot;/&amp;gt;

        &amp;lt;env name=&quot;PULSE_ENABLED&quot; value=&quot;false&quot;/&amp;gt;
        &amp;lt;env name=&quot;TELESCOPE_ENABLED&quot; value=&quot;false&quot;/&amp;gt;
        &amp;lt;env name=&quot;NIGHTWATCH_ENABLED&quot; value=&quot;false&quot;/&amp;gt;
    &amp;lt;/php&amp;gt;
&amp;lt;/phpunit&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Run Tests&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Before running tests:&lt;/p&gt;
&lt;p&gt;Ensure your testing database exists&lt;br&gt;
Run migrations if needed&lt;/p&gt;
&lt;p&gt;Then execute:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;php artisan test

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚡ Tips&lt;br&gt;
Isolation: APP_ENV=testing ensures .env.testing is used.&lt;br&gt;
Performance:&lt;br&gt;
CACHE_STORE=array&lt;br&gt;
SESSION_DRIVER=array&lt;br&gt;
QUEUE_CONNECTION=sync&lt;br&gt;
Faster Hashing:&lt;br&gt;
Reduce BCRYPT_ROUNDS during testing&lt;br&gt;
Disable Extras:&lt;br&gt;
Disable tools like Telescope, Pulse, etc., to speed up tests&lt;/p&gt;
&lt;p&gt;✅ Summary&lt;/p&gt;
&lt;p&gt;With this setup:&lt;/p&gt;
&lt;p&gt;Tests run faster ⚡&lt;br&gt;
Data remains safe &lt;br&gt;
Environment stays isolated &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13653/laravel-testing-environment-setup</guid>
<pubDate>Sun, 29 Mar 2026 14:36:21 +0000</pubDate>
</item>
<item>
<title>Leveraging Laravel with Docker for Streamlined Development</title>
<link>https://coderlegion.com/12501/leveraging-laravel-with-docker-for-streamlined-development</link>
<description>&lt;p&gt;Explore how to integrate Docker into your Laravel development workflow for enhanced efficiency and consistency.&lt;/p&gt;
&lt;p&gt;Setting up a development environment can be challenging. Using Docker with Laravel can simplify this process remarkably. Here’s how to get started:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install Docker&lt;/strong&gt;: Ensure you have Docker installed on your machine. Follow the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.docker.com/get-docker/&quot;&gt;official Docker documentation&lt;/a&gt; for your operating system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a Dockerfile&lt;/strong&gt;: In your Laravel project, create a &lt;code&gt;Dockerfile&lt;/code&gt; to define your application environment. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM php:8.0-fpm
WORKDIR /var/www
COPY . .
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev &amp;amp;&amp;amp; docker-php-ext-configure gd --with-freetype --with-jpeg &amp;amp;&amp;amp; docker-php-ext-install gd
CMD php artisan serve --host=0.0.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This example sets up PHP with GD support, essential for image processing in Laravel applications.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a docker-compose.yml file&lt;/strong&gt;: This file will manage your multi-container application. For Laravel, it might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  app:
    build:
      context: .
    volumes:
      - .:/var/www
    ports:
      - &quot;8000:8000&quot;
  db:
    image: mysql:5.7
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above example sets up a MySQL database alongside your Laravel app.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run the containers&lt;/strong&gt;: Execute the following command in your terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This command will bring up your Laravel application and your database as defined in the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access your application&lt;/strong&gt;: You can now access your Laravel application running on &lt;code&gt;http://localhost:8000&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;: By using Docker, you ensure a consistent development environment across different setups, leading to fewer deployment headaches. It’s a game-changer for collaborative projects where each developer may have different local setups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Take Action&lt;/strong&gt;: Try integrating Docker into your Laravel project today and notice how it changes your workflow for the better.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12501/leveraging-laravel-with-docker-for-streamlined-development</guid>
<pubDate>Mon, 09 Mar 2026 19:44:13 +0000</pubDate>
</item>
<item>
<title>Essential Laravel Tools for Efficient Development</title>
<link>https://coderlegion.com/12187/essential-laravel-tools-for-efficient-development</link>
<description>&lt;p&gt;Explore the must-have tools that every Laravel developer should consider for streamlined development and improved efficiency.&lt;/p&gt;
&lt;p&gt;As a Laravel developer, using the right tools can significantly enhance your productivity. Here are some essential tools worth checking out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tinker&lt;/strong&gt; - Laravel's built-in REPL allows you to interact with your application directly. You can quickly test models and run database queries without the overhead of a full server environment.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Laravel Debugbar&lt;/strong&gt; - This package offers an easy way to debug your application. It provides insights into your application's performance, route details, and queries executed, making it easier to identify bottlenecks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PHPStorm&lt;/strong&gt; - Although not exclusive to Laravel, PHPStorm provides excellent support for Laravel projects. Features like code completion, navigation, and refactoring tools streamline your coding process.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Laravel Mix&lt;/strong&gt; - Enhance your asset compilation with Laravel Mix. It enables you to use modern JavaScript libraries and preprocessors like Sass with minimal configuration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Postman&lt;/strong&gt; - Essential for API testing. With Postman, you can easily manage your API endpoints, conduct tests, and automate workflows, ensuring your APIs are robust and well-documented.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Git&lt;/strong&gt; - Version control is vital for team collaboration and maintaining code integrity. Familiarize yourself with Git commands to manage your project's development lifecycle effectively.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Laravel Envoy&lt;/strong&gt; - If you manage server deployments, Laravel Envoy simplifies the process. Use it to define deployment tasks as code, making deployments consistent and repeatable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Spatie Packages&lt;/strong&gt; - The Spatie team offers a suite of packages that add great functionality to Laravel applications, from media management to role and permission handling. Try them out to save time on common tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By integrating these tools into your workflow, you can enhance your capabilities as a Laravel developer and build applications that not only meet user expectations but exceed them.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12187/essential-laravel-tools-for-efficient-development</guid>
<pubDate>Thu, 26 Feb 2026 09:45:00 +0000</pubDate>
</item>
<item>
<title>How I optimized a web application early in my career</title>
<link>https://coderlegion.com/12076/how-i-optimized-a-web-application-early-in-my-career</link>
<description>&lt;p&gt;This happened nearly 6 years ago, around the year 2019. I was working on my first job at the time as a junior web developer.&lt;/p&gt;
&lt;p&gt;At that time I was working on an e-learning platform. My work mainly revolves around building and maintaining the marketing website for the platform. That includes home website, blogs and courses section. I was working in that project as the lead developer along with another front-end developer. &lt;em&gt;It’s a small startup, so I was involved on many things even though I was a junior developer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The project was migrated from Wordpress, and rebuilt with Laravel &amp;amp; MySQL. I handled most part of the migration that time.&lt;/p&gt;
&lt;p&gt;The website was smooth, everything working fine. &lt;em&gt;In my perspective at that time, if the website loads, then it was smooth. I was naive.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One day my team lead, &lt;em&gt;(There was only one team lead in the entire company)&lt;/em&gt; called me and explained his concerns regarding the website speed and performance. I didn’t understand it at first. The website was working fine. While it wasn’t loading at the same speed like a single-page application, it was decent enough. But for him, that wasn’t good enough.&lt;/p&gt;
&lt;p&gt;He told me to do whatever I can to optimize the website to load faster. He gave me three days to do it.&lt;/p&gt;
&lt;p&gt;At that moment, I was like a boy who had just been thrown into something much bigger.&lt;/p&gt;
&lt;p&gt;I was just slightly better than a beginner at best. I knew how to build features, I knew how to make things work. But scaling? Performance? Optimization? I hand no idea where to even begin.&lt;/p&gt;
&lt;p&gt;I couldn’t refuse the task. And honestly, I have no reason to refuse. He gave me complete freedom to research, experiment and approach the problem however I want.&lt;/p&gt;
&lt;p&gt;Even though I hesitated at first because I thought I might break things that were working fine, I eventually agreed to do it. But to be honest, I had no clue where to start.&lt;/p&gt;
&lt;p&gt;First day was all about researching and analyzing. I went through articles on Laravel optimization techniques, caching, query optimization, etc. I literally went through everything I found on Google.&lt;/p&gt;
&lt;p&gt;I’ve analyzed the website through multiple tools like Checkbot, GTMetrix, etc., to find what’s affecting the performance.&lt;/p&gt;
&lt;p&gt;I’ve listed out every suggested change I could. But honestly there wasn’t much I could do in the remaining two days.&lt;/p&gt;
&lt;p&gt;I couldn’t risk trying to optimize queries in this stage and breaking something. We are using Laravel eloquent queries for database operations and I barely know basic SQL querying at that time.&lt;/p&gt;
&lt;p&gt;I don’t know much about caching at that time and I don’t have the bandwidth to learn and implement it.&lt;/p&gt;
&lt;p&gt;There were quite a few of limitations on what I could do.&lt;/p&gt;
&lt;p&gt;First one is we are using shared hosting and hosted the website on cpanel. So, I can’t freely install additional software like Redis for caching. The server only supports PHP and MySQL.&lt;/p&gt;
&lt;p&gt;Second one is, the website was working perfectly fine and I couldn’t risk breaking anything in the perfectly working system.&lt;/p&gt;
&lt;p&gt;Third one is I’ve limited time. And whatever I do, I have to do it in the remaining two days.&lt;/p&gt;
&lt;p&gt;So, I’ve listed only the issue that I was confident to work on.&lt;/p&gt;
&lt;p&gt;a. This was the first and most important issue that I’ve listed. The website contains multiple components such as dynamic listing, infinite scrolling, dynamic filters, etc. And I was rendering all these contents through API from frontend at the time of page load using Javascript. This is one of the factors that’s increasing the page load time.&lt;/p&gt;
&lt;p&gt;b. In the current website, I am serving uncompressed styles and scripts, even though Laravel comes with Laravel Mix by default.&lt;/p&gt;
&lt;p&gt;c. Some of the images that were used in the website are of high quality and comes in large size, that were resizing on frontend from CSS.&lt;/p&gt;
&lt;p&gt;There were some other issues too, but these were the main issues among the ones that I remember.&lt;/p&gt;
&lt;p&gt;Then I’ve started working on the second day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) Reduced the usage of Javascript&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Logically this was the best thing to do. But honestly, many components are working dynamically front frontend. So, I couldn’t fully avoid the usage of Javascript.&lt;/p&gt;
&lt;p&gt;So I did this. I’ve listed out the content that’s dynamically on page load, and rendered it from server-side. This way I’ve mostly avoided API calls, and rendering content using Javascript on page load event. Now the DOM will only get modified when something’s changed or the events get triggered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) Building assets for production&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At that time, I am not familiar with things like webpack, mixing assets etc. So, even though Laravel comes with Laravel Mix, I’ve served assets normally from &lt;em&gt;/public&lt;/em&gt; folder.&lt;/p&gt;
&lt;p&gt;But after some digging, I’ve learned how to use Laravel Mix. Honestly, it was super easy and fun. I still use it as my default bundler for my projects, even though the repository is abandoned.&lt;/p&gt;
&lt;p&gt;I’ve removed all inline and uncompressed styles and scripts, moved them into the &lt;em&gt;/resources&lt;/em&gt; folder, centralized them properly, and ensured that only production-ready compiled assets were being served.&lt;/p&gt;
&lt;p&gt;It might sound small, but this reduced unnecessary load and made the application clearer and more maintainable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) Compressing Images&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is where it got little trickier. For the website, the client provides the images. Normally the images are of high quality and heavy-weighted and too large. So, we are embedding the images using image tag and resizing using CSS on frontend.&lt;/p&gt;
&lt;p&gt;After brainstorming a bit, I’ve decided to compress them and load the images with exact sizes to avoid the resizing from frontend. I’ve used &lt;em&gt;PHPIntervention&lt;/em&gt; library to compress and resize images from server side.&lt;/p&gt;
&lt;p&gt;I’ve created a new method something like this :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;function load_image($filename, $width, $height) {  
 $path = &quot;new folder for resized images&quot;;  
 $new_name = $width . &quot;_&quot; . $height . &quot;_&quot; . $filename;  
 if(!file_exists($path . &quot;/&quot; . $new_name)) {  
  resize_image_using_intervention();  
  return resized_image_path();  
 }  
  
 return resized_image_path();  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The role of this method is, it will check if the resized version of the given image is present in the file system and if not, it will compress and resize the image and store it in the file system. If exists, it will return the image URL as string.&lt;/p&gt;
&lt;p&gt;I can use in blade template like below :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;img src=&quot;{{ load_image(&quot;image.png&quot;, 200, 100) }}&quot; alt=&quot;&quot; /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And it will display the resized image. As it won’t touch the original image and create resized copies of it, I can use the same image with multiple sizes in multiple places.&lt;/p&gt;
&lt;p&gt;This way, the browser don’t need to adjust the image sizes on frontend as the images already loading with correct dimensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) Rendering Minified HTML&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Well, this isn’t actually a problem, but an extra I’ve decided to do. And this was easy, there were many packages available for Laravel to minify the templates before rendering. Honestly, I just went with the flow for this one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5) Gzip Compression&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At that time, I don’t even know anything about Gzip compression. I’ve found it in some article, and after looking into it, I’ve decided to give it a try, since I only needed to modify &lt;em&gt;.htaccess&lt;/em&gt; file and thats it. No code changes, nothing.&lt;/p&gt;
&lt;p&gt;That’s it. This is all I did and I’ve managed to do it in the given timeframe. And to my surprise, the speed of the page loading increased more than before, even client acknowledged it.&lt;/p&gt;
&lt;p&gt;Looking back now, compared to the optimization techniques and tools available today, what I did might seem small, or may be even basic.&lt;/p&gt;
&lt;p&gt;But at that time, I was barely more than a beginner. I don’t know advanced scaling strategies or deep performance engineering. I simply did what I was capable of doing with the knowledge I had. And luckily, it worked better than I expected.&lt;/p&gt;
&lt;p&gt;When I look back at that project, I also remember something else.&lt;/p&gt;
&lt;p&gt;The application was fully server-size rendered. I initially used web components, but later I removed them during optimization.&lt;/p&gt;
&lt;p&gt;Today, things feel very different.&lt;/p&gt;
&lt;p&gt;We ship large amounts of Javascript by default. Rendering logic that could easily live on the server is being pushed to the client. Using complete frontend frameworks has almost become the norm.&lt;/p&gt;
&lt;p&gt;There was even a time when I disliked PHP and SSR. I was drawn toward modern shiny frameworks like React and Vue, believing they were automatically better.&lt;/p&gt;
&lt;p&gt;But think what, after working on real-world projects that relied heavily on them, I’ve actually started to like and appreciate PHP and SSR more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Developers today are, in many cases, taking Javascript for granted.&lt;/p&gt;
&lt;p&gt;Javascript is really powerful, but it should be used for what it’s meant to be used. Handling dynbamic client-size interactions, managing complex UI state, and enhancing user experience are prefect use cases. But rendering everything on the client by default isn’t always the smartest choice.&lt;/p&gt;
&lt;p&gt;Forcing one language or one ecosystem into every layer of an application may look modern and clean, but we must consider what we are trading for it, bundle size, performance overhead, complexity, and long-term maintainability.&lt;/p&gt;
&lt;p&gt;I don’t hate modern technologies. In fact, I’ve worked with them and learned from them. What I’ve come to understand is simple. What I’ve come to understand is simple. Choose your stack wisely. Let the problem decide the tools, not the trends.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published at&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://bhaswanth.com/blog/how-i-optimized-php-project-in-3-days&quot;&gt;&lt;em&gt;https://bhaswanth.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12076/how-i-optimized-a-web-application-early-in-my-career</guid>
<pubDate>Mon, 23 Feb 2026 19:36:59 +0000</pubDate>
</item>
<item>
<title>Getting Started with GraphQL for Laravel Developers</title>
<link>https://coderlegion.com/12069/getting-started-with-graphql-for-laravel-developers</link>
<description>&lt;p&gt;Unlock the potential of GraphQL in your Laravel applications. This guide covers the fundamentals, advantages over REST, and practical ways to integrate GraphQL into your projects.&lt;/p&gt;
&lt;h3 id=&quot;introduction-to-graphql&quot;&gt;Introduction to GraphQL&lt;/h3&gt;
&lt;p&gt;GraphQL is a powerful query language for APIs, initially developed by Facebook. Unlike REST, which exposes multiple endpoints, GraphQL provides a single endpoint. Here, you can request only the data you need, reducing payload size and improving speed.&lt;/p&gt;
&lt;h3 id=&quot;architecture-of-graphql&quot;&gt;Architecture of GraphQL&lt;/h3&gt;
&lt;p&gt;At its core, GraphQL is built around the concepts of types and fields. You define a schema that describes what queries can be made and what types of data each query returns. This schema acts as a contract between the client and the server.&lt;/p&gt;
&lt;h4 id=&quot;key-components-of-graphql-&quot;&gt;Key Components of GraphQL:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Queries&lt;/strong&gt;: Read data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mutations&lt;/strong&gt;: Write data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Subscriptions&lt;/strong&gt;: Listen to real-time updates.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;advantages-over-rest&quot;&gt;Advantages Over REST&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Single Endpoint&lt;/strong&gt;: Eliminate the need for multiple URLs, streamlining your API.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client-Driven&lt;/strong&gt;: Clients request exactly what they need, minimizing over-fetching and under-fetching.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strongly Typed&lt;/strong&gt;: Changes to the data structure can be managed more easily due to the schema definition.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;h4 id=&quot;integrating-graphql-with-laravel&quot;&gt;Integrating GraphQL with Laravel&lt;/h4&gt;
&lt;p&gt;To start using GraphQL in your Laravel application, follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install the Package&lt;/strong&gt;:&lt;br&gt;
You can use the &lt;code&gt;lighthouse-php&lt;/code&gt; package, which is a popular choice for integrating GraphQL in Laravel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require nuwave/lighthouse
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publish the Config File&lt;/strong&gt;:&lt;br&gt;
Run the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;php artisan vendor:publish --tag=lighthouse
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define Your Schema&lt;/strong&gt;:&lt;br&gt;
Create a &lt;code&gt;graphql/schema.graphql&lt;/code&gt; file. Here’s a simple example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-graphql&quot;&gt;type Query {
    users: [User]
}
type User {
    id: ID!
    name: String!
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create Resolvers&lt;/strong&gt;:&lt;br&gt;
For the &lt;code&gt;users&lt;/code&gt; query, you might set up a resolver in &lt;code&gt;app/GraphQL/Queries/UserQuery.php&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;namespace App\GraphQL\Queries;
use App\Models\User;
   
class UserQuery {
    public function resolve($root, array $args) {
        return User::all();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing Your GraphQL API&lt;/strong&gt;:&lt;br&gt;
You can use tools like Postman or GraphQL Playground to play around with your API. Here’s a simple query to get started:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-graphql&quot;&gt;query {
    users {
        id
        name
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;GraphQL offers a flexible way to interact with your data, which can improve performance and reduce the time it takes to build features. By adopting these principles, you can create APIs that better serve the needs of your applications. Start experimenting, and you might find that GraphQL changes how you think about API design altogether.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12069/getting-started-with-graphql-for-laravel-developers</guid>
<pubDate>Mon, 23 Feb 2026 06:56:26 +0000</pubDate>
</item>
<item>
<title>Seamless Dropbox Integration in PHP &amp; Laravel: A Modern SDK Tutorial</title>
<link>https://coderlegion.com/11581/seamless-dropbox-integration-in-php-laravel-a-modern-sdk-tutorial</link>
<description>&lt;p&gt;You&amp;#39;ve been there. Tasked with integrating cloud storage, you find yourself wrestling with verbose API documentation, handling cumbersome OAuth 2.0 flows, and writing boilerplate code just to upload or share a file. While services like Dropbox offer powerful APIs, the integration process can often feel clunky and outdated, pulling you away from building the features that matter.&lt;/p&gt;
&lt;p&gt;This is where a modern, well-designed SDK becomes invaluable. For developers in the PHP ecosystem, especially those using Laravel, the &lt;code&gt;tigusigalpa/dropbox-php&lt;/code&gt; package emerges as a clean, intuitive, and powerful solution for streamlining Dropbox integration. This tutorial will walk you through how this library simplifies cloud storage operations, from basic file handling to advanced features like chunked uploads.&lt;/p&gt;
&lt;h3 id=&quot;why-a-modern-sdk-matters&quot;&gt;Why a Modern SDK Matters&lt;/h3&gt;
&lt;p&gt;In a landscape of rapidly evolving development practices, using a modern SDK is about more than just convenience; it&amp;#39;s about leveraging the full power of the language. Built for &lt;strong&gt;PHP 8.1+&lt;/strong&gt;, this library embraces modern features like typed properties and a clean, fluent API. This results in code that is not only easier to write but also more readable, maintainable, and less prone to errors compared to older libraries or manual cURL requests.&lt;/p&gt;
&lt;h3 id=&quot;core-features-at-a-glance&quot;&gt;Core Features at a Glance&lt;/h3&gt;
&lt;p&gt;The library is packed with features designed to provide a comprehensive and developer-friendly experience. It covers the entire Dropbox API v2, ensuring you have the tools for any task.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Feature&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;Full API v2 Coverage&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Access over 40 methods for files, folders, sharing, users, and Paper.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;Laravel 8-12+ Support&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Includes a service provider, facade, and publishable config for seamless framework integration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;Standalone Capability&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Works perfectly without any framework, making it versatile for any PHP project.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;Chunked Uploads&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Reliably upload large files (up to 350GB) by splitting them into smaller, manageable chunks.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;Batch Operations&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Efficiently copy, move, or delete up to 1,000 files in a single API call.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;strong&gt;OAuth 2.0 Flow&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Provides helpers to easily manage the complete authorization process, including token generation and refresh.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;getting-started-installation-configuration&quot;&gt;Getting Started: Installation &amp;amp; Configuration&lt;/h3&gt;
&lt;p&gt;Integrating the package into your project is straightforward. First, pull it in using Composer:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-shell&quot;&gt;&lt;span class=&quot;hljs-symbol&quot;&gt;composer&lt;/span&gt; &lt;span class=&quot;hljs-meta&quot;&gt;require&lt;/span&gt; tigusigalpa/dropbox-php
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;for-laravel-users&quot;&gt;For Laravel Users&lt;/h4&gt;
&lt;p&gt;The package automatically registers its service provider and facade. All you need to do is publish the configuration file and add your credentials to the &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Publish the configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-shell&quot;&gt;php artisan vendor:publish &lt;span class=&quot;hljs-comment&quot;&gt;--tag=dropbox-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update your &lt;code&gt;.env&lt;/code&gt; file:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-dotenv&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;DROPBOX_ACCESS_TOKEN&lt;/span&gt;=your_access_token_here
&lt;span class=&quot;hljs-attr&quot;&gt;DROPBOX_APP_KEY&lt;/span&gt;=your_app_key
&lt;span class=&quot;hljs-attr&quot;&gt;DROPBOX_APP_SECRET&lt;/span&gt;=your_app_secret
&lt;span class=&quot;hljs-attr&quot;&gt;DROPBOX_REDIRECT_URI&lt;/span&gt;=https://your-app.com/callback
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;practical-usage-a-step-by-step-guide&quot;&gt;Practical Usage: A Step-by-Step Guide&lt;/h3&gt;
&lt;p&gt;Let&amp;#39;s dive into some code. The library&amp;#39;s clean API makes performing common tasks incredibly simple.&lt;/p&gt;
&lt;h4 id=&quot;example-1-standalone-php-uploading-a-file&quot;&gt;Example 1: Standalone PHP - Uploading a File&lt;/h4&gt;
&lt;p&gt;Even without a framework, using the client is intuitive. After obtaining an access token from the Dropbox App Console, you can start making calls immediately.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-php&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Tigusigalpa&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Dropbox&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;DropboxClient&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;// Instantiate the client with your access token&lt;/span&gt;
$client = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; DropboxClient(&lt;span class=&quot;hljs-string&quot;&gt;'your_access_token'&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;// Get current user's account info&lt;/span&gt;
$account = $client-&amp;gt;users-&amp;gt;getCurrentAccount();
&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;Hello, &quot;&lt;/span&gt; . $account[&lt;span class=&quot;hljs-string&quot;&gt;'name'&lt;/span&gt;][&lt;span class=&quot;hljs-string&quot;&gt;'display_name'&lt;/span&gt;];

&lt;span class=&quot;hljs-comment&quot;&gt;// Upload a simple text file&lt;/span&gt;
$result = $client-&amp;gt;files-&amp;gt;upload(
    &lt;span class=&quot;hljs-string&quot;&gt;'/Apps/MyApp/hello.txt'&lt;/span&gt;,
    &lt;span class=&quot;hljs-string&quot;&gt;'Hello, World!'&lt;/span&gt;,
    &lt;span class=&quot;hljs-string&quot;&gt;'add'&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;// Use 'overwrite' to replace existing files&lt;/span&gt;
);

&lt;span class=&quot;hljs-keyword&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;File uploaded successfully: &quot;&lt;/span&gt; . $result[&lt;span class=&quot;hljs-string&quot;&gt;'path_display'&lt;/span&gt;];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;example-2-laravel-integration-file-uploads-in-a-controller&quot;&gt;Example 2: Laravel Integration - File Uploads in a Controller&lt;/h4&gt;
&lt;p&gt;Within a Laravel application, you can leverage either the &lt;code&gt;Dropbox&lt;/code&gt; facade or dependency injection for a clean, testable implementation. Here’s how you might handle a file upload from a user request.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;lang-php&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Illuminate&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Http&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Request&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Tigusigalpa&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Dropbox&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;DropboxClient&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;App&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Http&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Controllers&lt;/span&gt;\&lt;span class=&quot;hljs-title&quot;&gt;Controller&lt;/span&gt;;

&lt;span class=&quot;hljs-class&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;FileUploadController&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Controller&lt;/span&gt;
&lt;/span&gt;{
    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;store&lt;/span&gt;&lt;span class=&quot;hljs-params&quot;&gt;(Request $request, DropboxClient $dropbox)&lt;/span&gt;
    &lt;/span&gt;{
        $request-&amp;gt;validate([&lt;span class=&quot;hljs-string&quot;&gt;'document'&lt;/span&gt; =&amp;gt; &lt;span class=&quot;hljs-string&quot;&gt;'required|file'&lt;/span&gt;]);

        $file = $request-&amp;gt;file(&lt;span class=&quot;hljs-string&quot;&gt;'document'&lt;/span&gt;);
        $content = file_get_contents($file-&amp;gt;path());
        $path = &lt;span class=&quot;hljs-string&quot;&gt;'/uploads/'&lt;/span&gt; . $file-&amp;gt;getClientOriginalName();

        &lt;span class=&quot;hljs-comment&quot;&gt;// Upload the file to Dropbox&lt;/span&gt;
        $result = $dropbox-&amp;gt;files-&amp;gt;upload($path, $content);

        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; response()-&amp;gt;json([
            &lt;span class=&quot;hljs-string&quot;&gt;'message'&lt;/span&gt; =&amp;gt; &lt;span class=&quot;hljs-string&quot;&gt;'File uploaded successfully.'&lt;/span&gt;,
            &lt;span class=&quot;hljs-string&quot;&gt;'data'&lt;/span&gt; =&amp;gt; $result
        ]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tigusigalpa/dropbox-php&lt;/code&gt; library offers a robust and elegant solution for integrating the Dropbox API into modern PHP and Laravel applications. By providing a clean, fluent interface and handling the complexities of features like chunked uploads and batch operations, it allows developers to focus on building features rather than wrestling with API mechanics. Whether you&amp;#39;re building a simple file-sharing tool or a complex, enterprise-level application, this SDK provides the foundation you need for seamless and reliable cloud storage integration.&lt;/p&gt;
&lt;p&gt;Give it a try in your next project and experience a more productive way to work with Dropbox.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;p&gt;[1] &lt;code&gt;tigusigalpa/dropbox-php&lt;/code&gt; GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tigusigalpa/dropbox-php&quot;&gt;https://github.com/tigusigalpa/dropbox-php&lt;/a&gt;
[2] Official Wiki and Documentation: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tigusigalpa/dropbox-php/wiki&quot;&gt;https://github.com/tigusigalpa/dropbox-php/wiki&lt;/a&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11581/seamless-dropbox-integration-in-php-laravel-a-modern-sdk-tutorial</guid>
<pubDate>Fri, 13 Feb 2026 10:49:44 +0000</pubDate>
</item>
<item>
<title>Four Spaces Before &lt;?php</title>
<link>https://coderlegion.com/11383/four-spaces-before-php</link>
<description>&lt;p&gt;I was working on a Livewire component when every interaction started throwing 419 errors. CSRF token mismatch, on every click. The kind of thing that makes you question your setup immediately.&lt;/p&gt;
&lt;p&gt;At first I assumed it was a Livewire issue. But then I noticed something stranger — every page refresh generated a new session ID. Sessions worked fine with &lt;code&gt;php artisan serve&lt;/code&gt;, but the moment I switched to Herd, no cookies stuck. Each request was a clean slate.&lt;/p&gt;
&lt;p&gt;The 419 errors were just a symptom. Laravel couldn't maintain sessions because the browser never received a cookie.&lt;/p&gt;
&lt;h3 id=&quot;chasing-the-usual-suspects&quot;&gt;Chasing the usual suspects&lt;/h3&gt;
&lt;p&gt;I started with the &lt;code&gt;.env&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-env&quot;&gt;SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_DOMAIN=null
SESSION_SECURE_COOKIE=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Redis was running, PHP could connect to it. I created a test route to confirm sessions worked at the PHP level — they did. Data was stored, but the browser never got the &lt;code&gt;Set-Cookie&lt;/code&gt; header.&lt;/p&gt;
&lt;p&gt;I checked DevTools. No &lt;code&gt;laravel_session&lt;/code&gt; cookie. No &lt;code&gt;Set-Cookie&lt;/code&gt; header at all.&lt;/p&gt;
&lt;p&gt;I went through the usual list: cookie domain, HTTPS settings, session path, Redis connection. All fine. Switched to the file driver. Same result. Tried setting a cookie manually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::get('/cookie-test', function () {
    return response('Test')
        -&amp;gt;cookie('test_cookie', 'test_value', 60);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Still no &lt;code&gt;Set-Cookie&lt;/code&gt; header. At this point it clearly wasn't a Laravel config problem. Something was preventing PHP from sending cookies entirely.&lt;/p&gt;
&lt;h3 id=&quot;the-actual-problem&quot;&gt;The actual problem&lt;/h3&gt;
&lt;p&gt;The breakthrough came from a raw &lt;code&gt;header()&lt;/code&gt; call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::get('/header-test', function () {
    header('X-Custom-Test: working');
    // ...
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PHP responded with: &lt;em&gt;&quot;Cannot modify header information — headers already sent&quot;&lt;/em&gt;, and pointed to &lt;code&gt;routes/web.php:1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Line 1 should just be &lt;code&gt;&amp;lt;?php&lt;/code&gt;. I opened the file and there it was — the tag was indented. Four spaces sitting right there, plain as day. I just hadn't noticed.&lt;/p&gt;
&lt;h3 id=&quot;why-it-only-showed-up-in-herd&quot;&gt;Why it only showed up in Herd&lt;/h3&gt;
&lt;p&gt;The bug existed all along. &lt;code&gt;php artisan serve&lt;/code&gt; has output buffering enabled by default, so those spaces never reached the browser before the headers. The problem was silently masked.&lt;/p&gt;
&lt;p&gt;Herd uses nginx + PHP-FPM with &lt;code&gt;output_buffering = 0&lt;/code&gt;. Output goes out immediately. Those four spaces hit the browser before Laravel could send any headers — which meant no cookies, which meant no sessions.&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The fix&lt;/h3&gt;
&lt;p&gt;Delete four spaces. That's it.&lt;/p&gt;
&lt;p&gt;I made sure &lt;code&gt;routes/web.php&lt;/code&gt; started with &lt;code&gt;&amp;lt;?php&lt;/code&gt; on the very first character of the very first line. Saved. Everything worked.&lt;/p&gt;
&lt;p&gt;Livewire relies on the session to verify CSRF tokens on every request. Without a &lt;code&gt;Set-Cookie&lt;/code&gt; header, the browser never stores a session — and without a session, every Livewire interaction fails with a 419.&lt;/p&gt;
&lt;p&gt;Four spaces before &lt;code&gt;&amp;lt;?php&lt;/code&gt; were enough to start the response body, and once the body starts, PHP can't send headers anymore.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11383/four-spaces-before-php</guid>
<pubDate>Tue, 10 Feb 2026 13:35:08 +0000</pubDate>
</item>
<item>
<title>Structuring Large Laravel Projects Without Overengineering</title>
<link>https://coderlegion.com/11211/structuring-large-laravel-projects-without-overengineering</link>
<description>&lt;p&gt;In my experience , Laravel makes it incredibly easy to start a project. A few commands, some controllers, migrations, and you’re shipping features fast. The problem usually doesn’t appear in week one it shows up months later, when the codebase grows, the team expands, and simple changes start feeling risky.&lt;/p&gt;
&lt;p&gt;At that point, developers often swing to extremes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Either everything stays messy and tightly coupled&lt;/li&gt;
&lt;li&gt;Or the project becomes overengineered with layers, abstractions, and patterns no one fully understands&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sweet spot lies in between.&lt;/p&gt;
&lt;p&gt;This article explores &lt;strong&gt;how to structure large Laravel projects without overengineering&lt;/strong&gt;, keeping your codebase scalable, readable, and friendly to future contributors.&lt;/p&gt;
&lt;h3 id=&quot;the-real-problem-with-default-laravel-structure-&quot;&gt;The Real Problem With “Default Laravel Structure”&lt;/h3&gt;
&lt;p&gt;Laravel’s default structure is intentionally simple and that’s a good thing. But in larger projects, issues start to emerge:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controllers grow too large&lt;/li&gt;
&lt;li&gt;Business logic leaks everywhere&lt;/li&gt;
&lt;li&gt;Repeated patterns appear across features&lt;/li&gt;
&lt;li&gt;Debugging becomes harder than it should be&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The instinctive reaction is to introduce complex architectures too early. Unfortunately, that often creates more problems than it solves.&lt;/p&gt;
&lt;p&gt;The goal isn’t to make Laravel “enterprise looking.”&lt;br&gt;
The goal is to make it &lt;strong&gt;maintainable&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;start-with-features-not-layers&quot;&gt;Start With Features, Not Layers&lt;/h3&gt;
&lt;p&gt;One of the most common mistakes in large Laravel projects is organizing code strictly by technical layers.&lt;/p&gt;
&lt;p&gt;Instead of thinking:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Where do I put this service?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“What feature does this belong to?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;a-practical-feature-based-structure&quot;&gt;A Practical Feature Based Structure&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;app/
 ├── Features/
 │   ├── Orders/
 │   │   ├── Controllers/
 │   │   ├── Actions/
 │   │   ├── Requests/
 │   │   └── Policies/
 │   ├── Payments/
 │   └── Users/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps related logic close together and makes onboarding easier. When someone works on “Payments,” everything they need is in one place.&lt;/p&gt;
&lt;h3 id=&quot;keep-controllers-thin-but-don-t-panic-about-it-&quot;&gt;Keep Controllers Thin (But Don’t Panic About It)&lt;/h3&gt;
&lt;p&gt;Controllers shouldn’t contain complex business logic but that doesn’t mean every line needs a separate class.&lt;/p&gt;
&lt;p&gt;A good rule of thumb:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Controllers handle HTTP concerns&lt;/li&gt;
&lt;li&gt;Business logic lives elsewhere&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of massive controllers, extract logic into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Action classes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Service classes (when justified)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Domain specific helpers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function store(StoreOrderRequest $request)
{
    return CreateOrder::run($request &amp;gt;validated());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple, readable, and testable without unnecessary abstraction.&lt;/p&gt;
&lt;h3 id=&quot;use-services-only-when-logic-is-reused-or-complex&quot;&gt;Use Services Only When Logic Is Reused or Complex&lt;/h3&gt;
&lt;p&gt;Service classes are useful but only when they solve a real problem.&lt;/p&gt;
&lt;p&gt;Create a service when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Logic is reused across multiple features&lt;/li&gt;
&lt;li&gt;The operation is complex or multi step&lt;/li&gt;
&lt;li&gt;You need clear separation for testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid services that just wrap a single model call. That’s not architecture it’s ceremony.&lt;/p&gt;
&lt;h3 id=&quot;let-models-do-some-work-within-reason-&quot;&gt;Let Models Do Some Work (Within Reason)&lt;/h3&gt;
&lt;p&gt;Laravel models are powerful, and it’s okay to use them.&lt;/p&gt;
&lt;p&gt;Helpful patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Query scopes for reusable queries&lt;/li&gt;
&lt;li&gt;Model methods for domain specific behavior&lt;/li&gt;
&lt;li&gt;Accessors and mutators for data shaping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Avoid turning models into “god objects,” but don’t fear using them either. Balance matters more than purity.&lt;/p&gt;
&lt;h3 id=&quot;avoid-premature-design-patterns&quot;&gt;Avoid Premature Design Patterns&lt;/h3&gt;
&lt;p&gt;Patterns like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Repositories&lt;/li&gt;
&lt;li&gt;CQRS&lt;/li&gt;
&lt;li&gt;Event sourcing&lt;/li&gt;
&lt;li&gt;Hexagonal architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;are not bad but they are &lt;strong&gt;expensive&lt;/strong&gt; in complexity.&lt;/p&gt;
&lt;p&gt;If your team doesn’t feel the pain these patterns solve, adding them early will slow development and confuse contributors.&lt;/p&gt;
&lt;p&gt;Start simple. Introduce patterns &lt;strong&gt;only when a real problem appears&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;configuration-and-boundaries-matter-more-than-classes&quot;&gt;Configuration and Boundaries Matter More Than Classes&lt;/h3&gt;
&lt;p&gt;Large Laravel projects stay healthy when boundaries are clear.&lt;/p&gt;
&lt;p&gt;Focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consistent naming conventions&lt;/li&gt;
&lt;li&gt;Clear folder responsibilities&lt;/li&gt;
&lt;li&gt;Well defined APIs between features&lt;/li&gt;
&lt;li&gt;Shared code living in obvious locations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good structure is about &lt;strong&gt;clarity&lt;/strong&gt;, not class count.&lt;/p&gt;
&lt;h3 id=&quot;tests-are-part-of-the-structure&quot;&gt;Tests Are Part of the Structure&lt;/h3&gt;
&lt;p&gt;A well structured project is easy to test.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feature tests validate behavior&lt;/li&gt;
&lt;li&gt;Unit tests protect core logic&lt;/li&gt;
&lt;li&gt;Clear boundaries make mocking easier&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If testing feels painful, it’s often a sign the structure needs refinement not more abstractions.&lt;/p&gt;
&lt;h3 id=&quot;signs-you-re-overengineering&quot;&gt;Signs You’re Overengineering&lt;/h3&gt;
&lt;p&gt;If any of these sound familiar, it may be time to simplify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every request touches 6+ layers&lt;/li&gt;
&lt;li&gt;New developers struggle to find code&lt;/li&gt;
&lt;li&gt;Refactoring feels dangerous&lt;/li&gt;
&lt;li&gt;The architecture needs a diagram to explain&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good architecture fades into the background. It supports development it doesn’t dominate it.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts-structure-should-serve-the-team&quot;&gt;Final Thoughts: Structure Should Serve the Team&lt;/h3&gt;
&lt;p&gt;Structuring large Laravel projects isn’t about following trends or copying architectures from conference talks. It’s about building systems that &lt;strong&gt;real developers can understand, maintain, and evolve&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Start simple. Organize by features. Add structure only when it earns its place.&lt;/p&gt;
&lt;p&gt;If this article resonated with you, share it with a teammate who’s fighting an overcomplicated codebase. And if you enjoy practical Laravel content focused on real world tradeoffs, there’s plenty more worth exploring.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11211/structuring-large-laravel-projects-without-overengineering</guid>
<pubDate>Sat, 07 Feb 2026 10:58:01 +0000</pubDate>
</item>
<item>
<title>Vector: The easiest way to plug Vue in Blade</title>
<link>https://coderlegion.com/11004/vector-the-easiest-way-to-plug-vue-in-blade</link>
<description>&lt;p&gt;You know that feeling when you're building a Laravel app and you just need a tiny bit of reactivity? A counter. A toggle. Something that feels overkill for a full Vue component but too annoying for vanilla JavaScript?&lt;/p&gt;
&lt;p&gt;I kept reaching for Alpine.js, which is great, but I wanted Vue's Composition API. The &lt;code&gt;ref()&lt;/code&gt;, the &lt;code&gt;computed()&lt;/code&gt;, the familiar syntax I already know. So I bu4ilt Vector.&lt;/p&gt;
&lt;h3 id=&quot;what-even-is-this-&quot;&gt;What Even Is This?&lt;/h3&gt;
&lt;p&gt;Vector is a Laravel package that lets you write Vue directly in your Blade templates with zero ceremony:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
    const i = ref(0);
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
    &amp;lt;button @click=&quot;i++&quot;&amp;gt;Click Me&amp;lt;/button&amp;gt;
    &amp;lt;div&amp;gt;
        Count: @{{ i }}
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-if=&quot;i &amp;gt; 5&quot;&amp;gt;Success!&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole thing. No build step for your components. No separate &lt;code&gt;.vue&lt;/code&gt; files. No special directives wrapping your code. Just a &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; tag and you're done.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; tag gets transformed at compile time. Vector treats the &lt;strong&gt;element immediately after&lt;/strong&gt; the script tag as your Vue template. Everything inside that element becomes reactive, and anything outside it remains regular Blade.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Blade's precompiler finds your &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;Extracts your variable declarations&lt;/li&gt;
&lt;li&gt;Mounts Vue on the next sibling element&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key part is the variable extraction. It parses &lt;code&gt;const&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;, and &lt;code&gt;var&lt;/code&gt; declarations and auto-returns them to the template. You write normal code, it figures out the rest.&lt;/p&gt;
&lt;h4 id=&quot;escaping-blade-syntax&quot;&gt;Escaping Blade Syntax&lt;/h4&gt;
&lt;p&gt;Since Blade also uses &lt;code&gt;{{ }}&lt;/code&gt; for output, you need to prefix Vue's mustache syntax with &lt;code&gt;@&lt;/code&gt; to prevent Blade from processing it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;{{-- This is Blade --}}
{{ $phpVariable }}

{{-- This is Vue (note the @) --}}
@{{ vueVariable }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, use Vue directives like &lt;code&gt;v-text&lt;/code&gt; which don't conflict with Blade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;span v-text=&quot;count&quot;&amp;gt;&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;installation-1&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require brunoabpinto/vector
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add Vector to your Vite entry points in &lt;code&gt;vite.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;plugins: [
    laravel({
        input: [
            &quot;resources/css/app.css&quot;,
            &quot;resources/js/app.js&quot;,
            &quot;resources/js/vendor/vector.js&quot;,
        ],
        // ...
    }),
],
resolve: {
    alias: {
        'vue': 'vue/dist/vue.esm-bundler.js',
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;@vectorJs&lt;/code&gt; before your closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag in your layout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;body&amp;gt;
    {{ $slot }}

    @vectorJs
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. Vector auto-publishes its runtime, and &lt;code&gt;@vectorJs&lt;/code&gt; loads it where you need it.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-offs&quot;&gt;The Trade-offs&lt;/h3&gt;
&lt;p&gt;Let's be real about what this is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick interactive elements&lt;/li&gt;
&lt;li&gt;Prototyping&lt;/li&gt;
&lt;li&gt;When you want Vue's API without Vue's ceremony&lt;/li&gt;
&lt;li&gt;Laravel apps that are mostly server-rendered with islands of reactivity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not great for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex component hierarchies&lt;/li&gt;
&lt;li&gt;When you need proper SFC features (scoped styles, etc.)&lt;/li&gt;
&lt;li&gt;Large-scale SPAs (just use Inertia at that point)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-1&quot;&gt;Try It&lt;/h3&gt;
&lt;p&gt;The package is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/brunoabpinto/vector&quot;&gt;GitHub&lt;/a&gt;. Star it, fork it, tell me it's an abomination. Whatever feels right.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require brunoabpinto/vector
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11004/vector-the-easiest-way-to-plug-vue-in-blade</guid>
<pubDate>Tue, 03 Feb 2026 19:24:34 +0000</pubDate>
</item>
<item>
<title>I Turned Images Into 10,000 Tiny HTML Elements Because Why Not</title>
<link>https://coderlegion.com/10715/i-turned-images-into-10-000-tiny-html-elements-because-why-not</link>
<description>&lt;p&gt;&lt;em&gt;Sometimes the best projects are the ones that solve problems nobody has.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Last weekend, I found myself staring at my screen, thinking about images. Not in a productive way. Not &quot;how can I optimize image loading&quot; or &quot;what's the best format for web.&quot; No, I was thinking something far more unhinged:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if every pixel was just a tiny HTML element?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And thus, IMGML was born.&lt;/p&gt;
&lt;h3 id=&quot;the-premise&quot;&gt;The Premise&lt;/h3&gt;
&lt;p&gt;Here's the concept: take an image, read it pixel by pixel, and generate an HTML file where each pixel is represented by a 1x1 &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; element with a background color matching the original pixel.&lt;/p&gt;
&lt;p&gt;That's it. That's the whole thing.&lt;/p&gt;
&lt;p&gt;No &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tags. No &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;. No SVG. Just thousands upon thousands of microscopic horizontal rules, arranged in a grid, pretending to be an image.&lt;/p&gt;
&lt;h3 id=&quot;does-it-work-&quot;&gt;Does It Work?&lt;/h3&gt;
&lt;p&gt;Technically? Yes.&lt;/p&gt;
&lt;p&gt;Practically? That's a strong word.&lt;/p&gt;
&lt;p&gt;Upload a 100x100 pixel image and you get 10,000 HTML elements. A 200x200 image? 40,000 elements. Your browser will render it. Your browser might also ask you why you've done this.&lt;/p&gt;
&lt;p&gt;But here's the thing — it &lt;em&gt;does&lt;/em&gt; look like the image. Open the HTML file and there it is, your photo, reconstructed entirely from styled &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; tags. It's beautiful in the way that a Rube Goldberg machine is beautiful. Unnecessarily complex, deeply impractical, and yet somehow satisfying.&lt;/p&gt;
&lt;h3 id=&quot;why-though-&quot;&gt;Why Though?&lt;/h3&gt;
&lt;p&gt;I get asked this a lot. &quot;Why would you do this?&quot; &quot;What's the use case?&quot; &quot;Are you okay?&quot;&lt;/p&gt;
&lt;p&gt;The answers are: because I could, there isn't one, and debatable.&lt;/p&gt;
&lt;p&gt;This is a &quot;what if&quot; project. It exists in that space between curiosity and questionable judgment. The space where you wonder &quot;can I do this?&quot; before asking &quot;should I do this?&quot;&lt;/p&gt;
&lt;p&gt;Some might call it a waste of time. I call it a learning experience wrapped in absurdity.&lt;/p&gt;
&lt;h3 id=&quot;the-tech&quot;&gt;The Tech&lt;/h3&gt;
&lt;p&gt;For those curious, it's built with Laravel 12 and Livewire. Upload an image, the backend processes it pixel by pixel, and you download your freshly minted HTML monstrosity.&lt;/p&gt;
&lt;p&gt;Here's what the output looks like — just a wall of &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; tags, each with an inline background color:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;r&amp;gt;
  &amp;lt;hr style=&quot;background:#cad5d9&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#c5ced3&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#cdd6db&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#dde4ea&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#e4ebf1&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#e1e6ec&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#dadfe5&quot; /&amp;gt;
  &amp;lt;hr style=&quot;background:#d7dadf&quot; /&amp;gt;
  ...
&amp;lt;/r&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The code is straightforward. PHP's image functions handle the pixel reading, a loop builds the markup, and Livewire makes the upload/download flow seamless. Nothing revolutionary — just reasonable tools applied to an unreasonable idea.&lt;/p&gt;
&lt;h3 id=&quot;the-optimization-rabbit-hole&quot;&gt;The Optimization Rabbit Hole&lt;/h3&gt;
&lt;p&gt;Once I committed to this absurd idea, I became obsessed with making each pixel as small as possible. Here's how that went:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 1: RGB&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr style=&quot;background:rgb(255, 255, 255)&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Works, but verbose. All those characters add up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 2: Hex&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr style=&quot;background:#c5ced3&quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shorter. Hex codes are more compact than RGB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 3: Drop the self-closing slash&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr style=&quot;background:#c5ced3&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's optional in HTML5. Every byte counts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 4: Drop the quotes&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr style=background:#c5ced3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also valid HTML. We're getting minimal.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 5: The bgcolor attribute&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr bgcolor=&quot;#c5ced3&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deprecated, and for some reason only works on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. Dead end.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 6: The color attribute&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr color=#c5ced3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works. It's deprecated, it's not recommended, and it's perfect for this project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Attempt 7: Drop the hashtag&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;hr color=c5ced3&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Turns out the &lt;code&gt;#&lt;/code&gt; is optional on hex colors. One more character gone.&lt;/p&gt;
&lt;p&gt;This is the shortest I could make each pixel. If you know a way to make it smaller, I'm genuinely curious.&lt;/p&gt;
&lt;p&gt;The code is straightforward. PHP's image functions handle the pixel reading:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;// Resize to keep things manageable
$image = $this-&amp;gt;resizeImage($path, 200, 200);
$resource = imagecreatefromjpeg($image);

$width = imagesx($resource);
$height = imagesy($resource);
$pixels = [];

// Loop through every pixel
for ($y = 0; $y &amp;lt; $height; $y++) {
    for ($x = 0; $x &amp;lt; $width; $x++) {
        // Get color index at this coordinate
        $color = imagecolorat($resource, $x, $y);

        // Convert to RGB array
        $rgb = imagecolorsforindex($resource, $color);

        // Format as hex (no hashtag needed)
        $hex = sprintf('%02x%02x%02x', $rgb['red'], $rgb['green'], $rgb['blue']);

        $pixels[] = $hex;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Loop through each row, each column, grab the color, convert to hex, done. Livewire handles the upload/download flow. Nothing revolutionary — just reasonable tools applied to an unreasonable idea.&lt;/p&gt;
&lt;h3 id=&quot;what-i-actually-learned&quot;&gt;What I Actually Learned&lt;/h3&gt;
&lt;p&gt;Despite the silliness, building IMGML reminded me of a few things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Constraints breed creativity.&lt;/strong&gt; Limiting myself to just HTML elements forced me to think differently about rendering.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance matters everywhere.&lt;/strong&gt; Processing images pixel by pixel taught me to respect the work that actual image libraries do.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Side projects don't need purpose.&lt;/strong&gt; Not everything has to ship, scale, or solve a real problem. Sometimes you just build something weird and smile.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;should-you-use-this-&quot;&gt;Should You Use This?&lt;/h3&gt;
&lt;p&gt;Absolutely not.&lt;/p&gt;
&lt;p&gt;Well, maybe. If you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Confuse a web developer&lt;/li&gt;
&lt;li&gt;Create the world's least efficient image format&lt;/li&gt;
&lt;li&gt;Prove a point about HTML being turing complete (it's not, but this feels adjacent)&lt;/li&gt;
&lt;li&gt;Kill time on a Saturday&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then yes, IMGML is for you.&lt;/p&gt;
&lt;h3 id=&quot;try-it-yourself-1&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;The project is open source. Clone it, run it, upload an image, and watch your browser question its life choices as it renders your photo one &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; at a time.&lt;/p&gt;
&lt;p&gt;Will it change the world? No.&lt;/p&gt;
&lt;p&gt;Will it make you briefly happy in a &quot;wow, that's dumb&quot; kind of way? Almost certainly.&lt;/p&gt;
&lt;p&gt;And sometimes, that's enough.&lt;/p&gt;
&lt;p&gt;Here's &lt;a rel=&quot;nofollow&quot; href=&quot;https://placeholderblog.netlify.app/assets/blog/imgml.html&quot;&gt;my GitHub avatar in IMGML format&lt;/a&gt; if you want to see it in action.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;IMGML is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/brunoabpinto/IMGML&quot;&gt;GitHub&lt;/a&gt;. Star it if you appreciate the absurdity.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10715/i-turned-images-into-10-000-tiny-html-elements-because-why-not</guid>
<pubDate>Thu, 29 Jan 2026 23:34:43 +0000</pubDate>
</item>
<item>
<title>Top Laravel Development Tools Every Developer Needs in 2026</title>
<link>https://coderlegion.com/10630/top-laravel-development-tools-every-developer-needs-in-2026</link>
<description>&lt;p&gt;In 2026, web applications will be more complex than ever. Modern developers demand efficiency, scalability, reliability, and speed. Laravel development tools bridge gaps between code and deployment, providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Faster feedback loops&lt;/li&gt;
&lt;li&gt;Automated testing and quality checks&lt;/li&gt;
&lt;li&gt;Seamless local and cloud environments&lt;/li&gt;
&lt;li&gt;Enhanced debugging and monitoring&lt;/li&gt;
&lt;li&gt;Better team collaboration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using the right tools isn’t a luxury; it’s essential to maintaining high-quality codebases and delivering value quickly.&lt;/p&gt;
&lt;h2 id=&quot;best-laravel-development-tools-why-the-right-tools-matter-in-2026&quot;&gt;Best Laravel Development Tools: Why the Right Tools Matter in 2026&lt;/h2&gt;
&lt;p&gt;Choosing the right tools can significantly impact your &lt;a rel=&quot;nofollow&quot; href=&quot;https://wpwebinfotech.com/laravel-development/&quot;&gt;Laravel development&lt;/a&gt; workflow. In 2026, the top Laravel tools help developers create applications that are faster, cleaner, and easier to maintain.&lt;/p&gt;
&lt;h3 id=&quot;1-laravel-sail&quot;&gt;1. Laravel Sail&lt;/h3&gt;
&lt;p&gt;Laravel Sail is the official lightweight development environment for Laravel, powered by Docker. It abstracts away the complexity of container setup and provides a consistent environment across development teams.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-sail&quot;&gt;Why Developers Love Laravel Sail&lt;/h4&gt;
&lt;p&gt;Developers appreciate Sail because it eliminates the “works on my machine” problem. Sail ensures everyone on the team runs the same PHP version, services like MySQL/PostgreSQL, Redis, and more, all through Docker.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-sail&quot;&gt;Who Should Use Laravel Sail&lt;/h4&gt;
&lt;p&gt;Sail is perfect for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New Laravel developers who want a standardized environment&lt;/li&gt;
&lt;li&gt;Teams collaborating across different operating systems&lt;/li&gt;
&lt;li&gt;Projects that rely on multiple services (e.g., queues, caches, search)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Zero configuration, start sail up spins up services instantly&lt;/li&gt;
&lt;li&gt;Supports multiple service containers (MySQL, Redis, Meilisearch)&lt;/li&gt;
&lt;li&gt;Customizable PHP versions&lt;/li&gt;
&lt;li&gt;Works with major OS environments&lt;/li&gt;
&lt;li&gt;Integrated testing environment&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Developers consistently rate Sail highly for ease of use. On community forums, over 90% of users say Sail drastically improved setup time and reduced environment-related bugs.&lt;/p&gt;
&lt;h3 id=&quot;2-laravel-octane&quot;&gt;2. Laravel Octane&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://wpwebinfotech.com/blog/laravel-octane-to-scale-your-application/&quot;&gt;Laravel Octane&lt;/a&gt; supercharges Laravel applications with lightning-fast performance by leveraging high-performance application servers like RoadRunner and Swoole.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-octane&quot;&gt;Why Developers Love Laravel Octane&lt;/h4&gt;
&lt;p&gt;Octane keeps the application bootstrapped in memory, reducing overhead per request. This results in huge performance gains, often 2x to 10x faster than traditional request cycles.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-octane&quot;&gt;Who Should Use Laravel Octane&lt;/h4&gt;
&lt;p&gt;Octane is ideal for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;High-traffic websites&lt;/li&gt;
&lt;li&gt;Real-time applications&lt;/li&gt;
&lt;li&gt;API-first backend services&lt;/li&gt;
&lt;li&gt;Developers keen on performance optimization&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-1&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Persistent application memory&lt;/li&gt;
&lt;li&gt;Worker-based architecture&lt;/li&gt;
&lt;li&gt;Faster queue processing&lt;/li&gt;
&lt;li&gt;Better throughput and lower latency&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-1&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Octane is widely praised for its performance enhancements. Developers building high-scale apps often cite a dramatic improvement in response times after enabling Octane.&lt;/p&gt;
&lt;h3 id=&quot;3-laravel-debugbar&quot;&gt;3. Laravel Debugbar&lt;/h3&gt;
&lt;p&gt;Debugbar is a development tool that visually displays debugging and profiling information within your browser.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-debugbar&quot;&gt;Why Developers Love Laravel Debugbar&lt;/h4&gt;
&lt;p&gt;Debugbar gives real-time insights into SQL queries, route details, render times, cache usage, and more without leaving the app. It’s invaluable during development and debugging sessions.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-debugbar&quot;&gt;Who Should Use Laravel Debugbar&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Developers focused on debugging performance issues&lt;/li&gt;
&lt;li&gt;Teams optimizing database queries&lt;/li&gt;
&lt;li&gt;Anyone who wants visibility into application internals&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-2&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Query logs with execution time&lt;/li&gt;
&lt;li&gt;View rendered templates&lt;/li&gt;
&lt;li&gt;Exception tracking&lt;/li&gt;
&lt;li&gt;Performance profiling&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-2&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Laravel Debugbar is considered a staple in the developer toolset. Many report that it helps catch database inefficiencies early in development.&lt;/p&gt;
&lt;h3 id=&quot;4-laravel-forge&quot;&gt;4. Laravel Forge&lt;/h3&gt;
&lt;p&gt;Laravel Forge is a server provisioning and deployment tool that allows you to deploy Laravel applications without managing servers manually.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-forge&quot;&gt;Why Developers Love Laravel Forge&lt;/h4&gt;
&lt;p&gt;Forge abstracts away server configuration, enabling one-click deployments to popular cloud providers like DigitalOcean, AWS, and Linode.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-forge&quot;&gt;Who Should Use Laravel Forge&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Freelancers and small teams without dedicated DevOps&lt;/li&gt;
&lt;li&gt;Developers deploying to cloud servers frequently&lt;/li&gt;
&lt;li&gt;Projects requiring SSL, queue workers, and scheduled jobs&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-3&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Automated server provisioning&lt;/li&gt;
&lt;li&gt;One-click deployment&lt;/li&gt;
&lt;li&gt;SSL certificate management&lt;/li&gt;
&lt;li&gt;Queue and cron management&lt;/li&gt;
&lt;li&gt;Service monitoring&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-3&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Forge earns high marks for simplifying deployments. Many developers highlight its intuitive interface and reliable performance for production deployments.&lt;/p&gt;
&lt;h3 id=&quot;5-laravel-envoy&quot;&gt;5. Laravel Envoy&lt;/h3&gt;
&lt;p&gt;Envoy provides a simple way to run remote tasks in your Laravel projects via SSH, without writing full scripts.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-envoy&quot;&gt;Why Developers Love Laravel Envoy&lt;/h4&gt;
&lt;p&gt;Envoy makes automation tasks (deployments, migrations, cache clears) easy using elegant Blade-like syntax.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-envoy&quot;&gt;Who Should Use Laravel Envoy&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Developers who want lightweight deployment scripts&lt;/li&gt;
&lt;li&gt;Teams without full CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Anyone automating common server tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-4&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Simple task definitions&lt;/li&gt;
&lt;li&gt;SSH-based execution&lt;/li&gt;
&lt;li&gt;Reusable scripts&lt;/li&gt;
&lt;li&gt;Clean syntax&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-4&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Envoy’s simplicity is its biggest asset. Developers enjoy how straightforward it is compared to traditional shell scripting.&lt;/p&gt;
&lt;h3 id=&quot;6-laravel-telescope&quot;&gt;6. Laravel Telescope&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/12.x/telescope&quot;&gt;Laravel Telescope&lt;/a&gt; is a powerful debugging assistant that provides real-time insights into your application.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-laravel-telescope&quot;&gt;Why Developers Love Laravel Telescope&lt;/h4&gt;
&lt;p&gt;It delivers deep visibility into requests, logs, database queries, cache operations, queued jobs, and more all in an elegant UI.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-laravel-telescope&quot;&gt;Who Should Use Laravel Telescope&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Developers needing real-time diagnostics&lt;/li&gt;
&lt;li&gt;Those optimizing performance&lt;/li&gt;
&lt;li&gt;Teams debugging complex workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-5&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Request and exception monitoring&lt;/li&gt;
&lt;li&gt;Query performance&lt;/li&gt;
&lt;li&gt;Redis and cache insights&lt;/li&gt;
&lt;li&gt;Queue job tracking&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-5&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;The telescope is consistently rated among the most useful Laravel development tools for debugging at scale.&lt;/p&gt;
&lt;h3 id=&quot;7-phpstorm-with-laravel-plugin&quot;&gt;7. PHPStorm with Laravel Plugin&lt;/h3&gt;
&lt;p&gt;While not Laravel-specific, PHPStorm, combined with a dedicated Laravel plugin,s is a powerful IDE choice for Laravel developers.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-phpstorm&quot;&gt;Why Developers Love PHPStorm&lt;/h4&gt;
&lt;p&gt;PHPStorm offers intelligent code completion, refactoring tools, deep framework support, and integrated debugging, crucial for professional development.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-phpstorm&quot;&gt;Who Should Use PHPStorm&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Professional developers&lt;/li&gt;
&lt;li&gt;Teams standardizing on a powerful IDE&lt;/li&gt;
&lt;li&gt;Large codebases needing code intelligence&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-6&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Laravel route and view navigation&lt;/li&gt;
&lt;li&gt;Blade and PHP support&lt;/li&gt;
&lt;li&gt;Database tools&lt;/li&gt;
&lt;li&gt;Integrated test runner&lt;/li&gt;
&lt;li&gt;Refactoring tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-6&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;PHPStorm is widely regarded as the top IDE for Laravel development, with many developers claiming it significantly increases productivity.&lt;/p&gt;
&lt;h3 id=&quot;8-livewire-amp-inertia&quot;&gt;8. Livewire &amp;amp; Inertia&lt;/h3&gt;
&lt;p&gt;While technically libraries, Livewire and Inertia have become essential Laravel development tools for building reactive UIs with minimal JavaScript.&lt;/p&gt;
&lt;h4 id=&quot;why-developers-love-livewire-amp-inertia&quot;&gt;Why Developers Love Livewire &amp;amp; Inertia&lt;/h4&gt;
&lt;p&gt;They allow developers to build modern interfaces without abandoning server-driven workflows.&lt;/p&gt;
&lt;h4 id=&quot;who-should-use-them&quot;&gt;Who Should Use Them&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Teams building dynamic interfaces&lt;/li&gt;
&lt;li&gt;Developers preferring PHP-centric UI logic&lt;/li&gt;
&lt;li&gt;Projects avoiding full SPA frameworks&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-features-and-benefits-7&quot;&gt;Key Features and Benefits&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Component-based UI&lt;/li&gt;
&lt;li&gt;Reactive updates&lt;/li&gt;
&lt;li&gt;Blade integration&lt;/li&gt;
&lt;li&gt;Minimal JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ratings-and-developer-feedback-7&quot;&gt;Ratings and Developer Feedback&lt;/h4&gt;
&lt;p&gt;Both are community favorites. Many Laravel developers praise how they simplify SPA-like experiences without complexity.&lt;/p&gt;
&lt;h2 id=&quot;conclusion-levelling-up-with-laravel-development-tools&quot;&gt;Conclusion: Levelling Up with Laravel Development Tools&lt;/h2&gt;
&lt;p&gt;In 2026, leveraging the right Laravel development tools is crucial for building maintainable, scalable, and high-performing applications. From environment setup with Sail to deployment automation with Forge, and debugging with Telescope and Debugbar, the modern Laravel ecosystem has never been stronger.&lt;/p&gt;
&lt;p&gt;Whether you’re a solo developer or working with a professional &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/pulse/best-laravel-development-companies-india-2026-archit-prajapati-hpbsf/&quot;&gt;Laravel development company&lt;/a&gt;, integrating these tools into your workflow helps save time, reduce bugs, and significantly improve overall development efficiency.&lt;/p&gt;
&lt;p&gt;Ready to supercharge your Laravel projects? Start with the tools above and build with confidence in 2026.&lt;/p&gt;
&lt;h2 id=&quot;frequently-asked-questions-faqs-&quot;&gt;Frequently Asked Questions (FAQs)&lt;/h2&gt;
&lt;h3 id=&quot;1-what-are-laravel-development-tools-and-why-are-they-important-&quot;&gt;1. What are Laravel development tools, and why are they important?&lt;/h3&gt;
&lt;p&gt;Laravel development tools help developers build, test, debug, and deploy Laravel applications faster and more efficiently. They improve code quality, reduce errors, and streamline the overall development workflow.&lt;/p&gt;
&lt;h3 id=&quot;2-which-are-the-best-laravel-development-tools-for-beginners-in-2026-&quot;&gt;2. Which are the best Laravel development tools for beginners in 2026?&lt;/h3&gt;
&lt;p&gt;The Best Laravel Development Tools for beginners include Laravel Sail, Laravel Debugbar, and PHPStorm with Laravel plugins. These tools simplify environment setup, debugging, and coding for new developers.&lt;/p&gt;
&lt;h3 id=&quot;3-how-do-i-choose-the-right-laravel-development-tools-for-my-project-&quot;&gt;3. How do I choose the right Laravel development tools for my project?&lt;/h3&gt;
&lt;p&gt;Select Laravel development tools based on your project size, performance needs, and team experience. Small projects need basic tools, while large or high-traffic apps benefit from advanced monitoring and deployment tools.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10630/top-laravel-development-tools-every-developer-needs-in-2026</guid>
<pubDate>Wed, 28 Jan 2026 11:01:38 +0000</pubDate>
</item>
<item>
<title>Happy New Year, Laravel Developers! Welcome to 2026</title>
<link>https://coderlegion.com/9197/happy-new-year-laravel-developers-welcome-to-2026</link>
<description>&lt;p&gt;As we step into a brand new year, I want to officially welcome everyone back and wish you a year filled with growth, learning, clean code, and successful deployments &lt;/p&gt;
&lt;p&gt;This group remains a space for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sharing Laravel tips, patterns, and best practices&lt;/li&gt;
&lt;li&gt;Asking questions (no matter how small)&lt;/li&gt;
&lt;li&gt;Showcasing projects and ideas&lt;/li&gt;
&lt;li&gt;Helping each other grow as developers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether you’re just starting with Laravel or you’ve been building production apps for years, your contributions matter here.&lt;/p&gt;
&lt;p&gt;Let’s make 2026 a year of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Better architecture&lt;/li&gt;
&lt;li&gt;Scalable applications&lt;/li&gt;
&lt;li&gt;Strong community support&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Feel free to introduce yourself in the comments—what are you building this year with Laravel?&lt;/p&gt;
&lt;p&gt;Once again, welcome, and happy coding! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/9197/happy-new-year-laravel-developers-welcome-to-2026</guid>
<pubDate>Fri, 02 Jan 2026 12:22:49 +0000</pubDate>
</item>
<item>
<title>Mastering Laravel Task Scheduling for Automated Jobs (Compatible with Laravel 10, 11 &amp; 12)</title>
<link>https://coderlegion.com/6960/mastering-laravel-task-scheduling-for-automated-jobs-compatible-with-laravel-10-11-12</link>
<description>&lt;p&gt;If you’ve ever wanted your Laravel app to automatically perform tasks — like sending daily reports, clearing logs, or running database cleanups — &lt;strong&gt;task scheduling&lt;/strong&gt; is exactly what you need.&lt;/p&gt;
&lt;p&gt;Laravel makes this incredibly easy. No need for messy cron scripts or complex automation tools — everything is neatly handled right inside the framework.&lt;/p&gt;
&lt;p&gt;In this post, I’ll walk you through &lt;strong&gt;how Laravel’s Task Scheduler works&lt;/strong&gt;, step-by-step, so you can start automating jobs confidently. Whether you’re using &lt;strong&gt;Laravel 10, 11, or 12&lt;/strong&gt;, this guide has you covered.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-task-scheduling-in-laravel-&quot;&gt;What is Task Scheduling in Laravel?&lt;/h3&gt;
&lt;p&gt;Task Scheduling allows your application to automatically perform repetitive tasks at specific intervals — &lt;strong&gt;hourly, daily, weekly, or even every few minutes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of it as Laravel’s built-in personal assistant.&lt;/p&gt;
&lt;p&gt;Instead of manually running commands every time, Laravel’s scheduler lets you define everything in code — so tasks just happen, reliably.&lt;/p&gt;
&lt;p&gt;For example, you could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send daily email reports.&lt;/li&gt;
&lt;li&gt;Clean up old database records.&lt;/li&gt;
&lt;li&gt;Sync data from APIs.&lt;/li&gt;
&lt;li&gt;Generate invoices automatically every midnight.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-step-1-understand-how-the-scheduler-works&quot;&gt;⚙️ Step 1: Understand How the Scheduler Works&lt;/h3&gt;
&lt;p&gt;Laravel’s scheduler is powered by a single &lt;strong&gt;cron job&lt;/strong&gt; on your server. Once that cron job runs every minute, Laravel handles the rest — deciding which tasks should run and when.&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;One cron job to rule them all. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You don’t have to create multiple cron jobs for different tasks — just one that triggers Laravel’s internal scheduler.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-setting-up-the-cron-job&quot;&gt;Step 2: Setting Up the Cron Job&lt;/h3&gt;
&lt;p&gt;First, open your terminal and type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;crontab -e
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then add this line at the end of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;* * * * * cd /path-to-your-laravel-app &amp;amp;&amp;amp; php artisan schedule:run &amp;gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s what each part means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;* * * * *&lt;/code&gt; → Run every minute.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cd /path-to-your-laravel-app&lt;/code&gt; → Change directory to your Laravel project.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;php artisan schedule:run&lt;/code&gt; → Triggers Laravel’s scheduler.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt; → Silently discard output (to keep logs clean).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once that’s saved, your Laravel app can automatically run any scheduled tasks you define in code.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-create-your-first-scheduled-task&quot;&gt;Step 3: Create Your First Scheduled Task&lt;/h3&gt;
&lt;p&gt;All task definitions live inside this file:&lt;br&gt;
&lt;code&gt;app/Console/Kernel.php&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open it and find the &lt;code&gt;schedule()&lt;/code&gt; method. You’ll see something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;protected function schedule(Schedule $schedule)
{
    // Define your tasks here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, let’s create a simple example — maybe we want to log a message every hour.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;protected function schedule(Schedule $schedule)
{
    $schedule-&amp;gt;call(function () {
        \Log::info('Hourly job ran successfully at ' . now());
    })-&amp;gt;hourly();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it! &lt;/p&gt;
&lt;p&gt;Laravel will now automatically log this message every hour.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-scheduling-artisan-commands&quot;&gt;Step 4: Scheduling Artisan Commands&lt;/h3&gt;
&lt;p&gt;You can also schedule Artisan commands — for example, to run &lt;code&gt;php artisan backup:run&lt;/code&gt; every night:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('backup:run')-&amp;gt;dailyAt('01:00');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or a custom command you’ve written yourself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('emails:send')-&amp;gt;everyThirtyMinutes();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Laravel 11 &amp;amp; 12&lt;/strong&gt; make this even smoother with improved method chaining, so you can easily attach conditions or environments like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('queue:work')
         -&amp;gt;withoutOverlapping()
         -&amp;gt;onOneServer()
         -&amp;gt;everyMinute();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-scheduling-shell-commands&quot;&gt;Step 5: Scheduling Shell Commands&lt;/h3&gt;
&lt;p&gt;Sometimes, you may want to run system-level commands, such as clearing logs or syncing files.&lt;br&gt;
You can use the &lt;code&gt;exec()&lt;/code&gt; method for that:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;exec('rm -rf /var/logs/old_logs')-&amp;gt;weekly();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just be careful when using system commands — always double-check your paths and permissions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-6-adding-notifications-and-error-handling&quot;&gt;Step 6: Adding Notifications and Error Handling&lt;/h3&gt;
&lt;p&gt;What if something goes wrong during a scheduled task?&lt;/p&gt;
&lt;p&gt;Laravel lets you automatically send notifications when tasks fail or succeed.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('inspire')
         -&amp;gt;daily()
         -&amp;gt;emailOutputTo('*Emails are not allowed*')
         -&amp;gt;sendOutputTo(storage_path('logs/inspire.log'))
         -&amp;gt;onFailure(function () {
             \Log::error('The scheduled Inspire command failed!');
         });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also notify through Slack, Telegram, or custom integrations. (Check Laravel’s Notification system for advanced options!)&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-7-automating-database-maintenance&quot;&gt;Step 7: Automating Database Maintenance&lt;/h3&gt;
&lt;p&gt;Here’s a real-world example — let’s clean up old user sessions daily:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;call(function () {
    DB::table('sessions')-&amp;gt;where('last_activity', '&amp;lt;', now()-&amp;gt;subDays(7)-&amp;gt;timestamp)-&amp;gt;delete();
})-&amp;gt;daily();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This automatically deletes inactive sessions every day at midnight — hands-free!&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bonus-tips&quot;&gt;Bonus Tips&lt;/h3&gt;
&lt;h4 id=&quot;use-withoutoverlapping-&quot;&gt;Use &lt;code&gt;withoutOverlapping()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Prevents the same job from running multiple times if it hasn’t finished yet.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('emails:send')-&amp;gt;everyFiveMinutes()-&amp;gt;withoutOverlapping();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;use-ononeserver-&quot;&gt;Use &lt;code&gt;onOneServer()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Perfect for multi-server environments — ensures the job runs only once.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('reports:generate')-&amp;gt;daily()-&amp;gt;onOneServer();
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;use-runinbackground-&quot;&gt;Use &lt;code&gt;runInBackground()&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Allows Laravel to keep executing other tasks while one runs in the background.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$schedule-&amp;gt;command('backup:run')-&amp;gt;daily()-&amp;gt;runInBackground();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-conclusion&quot;&gt;✅ Conclusion&lt;/h3&gt;
&lt;p&gt;Laravel’s Task Scheduler is one of the framework’s most powerful (and often underrated) features.&lt;br&gt;
By setting it up once, you can fully automate repetitive tasks — whether it’s sending emails, cleaning data, or backing up files.&lt;/p&gt;
&lt;p&gt;And the best part? It’s &lt;strong&gt;simple&lt;/strong&gt;, &lt;strong&gt;elegant&lt;/strong&gt;, and &lt;strong&gt;works seamlessly&lt;/strong&gt; across Laravel 10, 11, and 12.&lt;/p&gt;
&lt;p&gt;So next time you find yourself running the same command daily, let Laravel do the heavy lifting. ⚙️&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/scheduling&quot;&gt;Official Laravel Scheduler Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel.com/docs/queues&quot;&gt;Laravel Queues &amp;amp; Jobs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://laravel-news.com&quot;&gt;Laravel 12 Preview Features&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Task Scheduling</category>
<guid isPermaLink="true">https://coderlegion.com/6960/mastering-laravel-task-scheduling-for-automated-jobs-compatible-with-laravel-10-11-12</guid>
<pubDate>Wed, 29 Oct 2025 10:45:59 +0000</pubDate>
</item>
<item>
<title>Building a Robust API with Laravel, Clean Architecture, and SOLID Principles</title>
<link>https://coderlegion.com/6596/building-a-robust-api-with-laravel-clean-architecture-and-solid-principles</link>
<description>&lt;p&gt;Excited to share a recent back-end project I architected and developed: the &lt;strong&gt;Favorite Products API&lt;/strong&gt;. This isn't just another CRUD API; it's a practical case study on implementing &lt;strong&gt;Clean Architecture&lt;/strong&gt; and &lt;strong&gt;SOLID&lt;/strong&gt; principles with Laravel to build a secure, high-performance, and scalable solution.&lt;/p&gt;
&lt;p&gt;The project is a RESTful API designed to manage user's favorite product lists, integrating with an external e-commerce service.&lt;/p&gt;
&lt;h4 id=&quot;-key-highlights-amp-challenges-overcome&quot;&gt;✨ Key Highlights &amp;amp; Challenges Overcome&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance Optimization (N+1 Problem):&lt;/strong&gt; I refactored the service layer to transform N+1 external API calls into a single, efficient bulk request (&lt;code&gt;findProductsByIds&lt;/code&gt;), dramatically reducing latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security First (IDOR Vulnerability):&lt;/strong&gt; We patched a critical &lt;em&gt;Insecure Direct Object Reference&lt;/em&gt; vulnerability by implementing strict, ownership-based authorization rules in the Form Requests. This is reinforced with feature tests covering 200, 401, and 403 status codes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decoupled &amp;amp; Testable Architecture:&lt;/strong&gt; By using &lt;em&gt;Dependency Inversion&lt;/em&gt; (Contracts/Interfaces), we fully decoupled our business logic from the external API client. This makes unit testing a breeze (just mock the interface!) and swapping data sources trivial.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilient Error Handling:&lt;/strong&gt; Instead of letting external service failures fail silently (returning an empty &lt;code&gt;200 OK&lt;/code&gt;), the system now throws custom exceptions, which are caught by a global handler to return a meaningful &lt;code&gt;503 Service Unavailable&lt;/code&gt; response.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The entire stack runs on &lt;strong&gt;PHP 8.3/Laravel 12&lt;/strong&gt;, &lt;strong&gt;PostgreSQL&lt;/strong&gt;, and is containerized with &lt;strong&gt;Docker&lt;/strong&gt;. We also have a comprehensive test suite using &lt;strong&gt;Pest&lt;/strong&gt; and auto-generated, interactive documentation with &lt;strong&gt;OpenAPI (Swagger)&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;open-source&quot;&gt;Open Source&lt;/h4&gt;
&lt;p&gt;The project is fully open-source. Feel free to explore the code, learn from the architectural decisions, or even contribute!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Check out the full source code on GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/lfrichter/favorite-products-api&quot;&gt;lfrichter/favorite-products-api&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope this can be a useful resource for anyone looking to deepen their understanding of modern back-end architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #laravel #php #backend #webdev #architecture&lt;/p&gt;
</description>
<category>API Dev</category>
<guid isPermaLink="true">https://coderlegion.com/6596/building-a-robust-api-with-laravel-clean-architecture-and-solid-principles</guid>
<pubDate>Fri, 17 Oct 2025 15:44:17 +0000</pubDate>
</item>
<item>
<title>Protecting Your Laravel API from DDoS Attacks: Best Practices</title>
<link>https://coderlegion.com/6393/protecting-your-laravel-api-from-ddos-attacks-best-practices</link>
<description>&lt;p&gt;When you deploy your Laravel API to production, it becomes exposed to the wild — and with that comes the risk of &lt;strong&gt;Distributed Denial of Service (DDoS)&lt;/strong&gt; attacks. These attacks can overwhelm your application with traffic, slow down your API, or even take your entire server offline.&lt;/p&gt;
&lt;p&gt;As Laravel developers, understanding how to &lt;strong&gt;prevent and mitigate&lt;/strong&gt; these attacks is critical. In this guide, we’ll explore how to use &lt;strong&gt;Laravel’s built-in features&lt;/strong&gt;, server configurations, and external tools to protect your API from overloads.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;understanding-ddos-attacks&quot;&gt;Understanding DDoS Attacks&lt;/h3&gt;
&lt;p&gt;A &lt;strong&gt;DDoS (Distributed Denial of Service)&lt;/strong&gt; attack happens when multiple systems flood a server or API with fake requests, exhausting its resources.&lt;br&gt;
In simpler terms — it’s like having thousands of people knocking on your door at once so real visitors can’t get in.&lt;/p&gt;
&lt;p&gt;There are two main types of DDoS attacks developers should know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Network-Level Attacks (Layer 3 &amp;amp; 4):&lt;/strong&gt; Flooding your server with packets to choke bandwidth.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application-Level Attacks (Layer 7):&lt;/strong&gt; Targeting your web app or API directly with valid-looking HTTP requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Our focus will be on &lt;strong&gt;Layer 7&lt;/strong&gt;, which is where Laravel operates.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;laravel-level-protection&quot;&gt;Laravel-Level Protection&lt;/h3&gt;
&lt;p&gt;Let’s start with what Laravel gives us right out of the box.&lt;/p&gt;
&lt;h4 id=&quot;1-rate-limiting-middleware&quot;&gt;&lt;strong&gt;1. Rate Limiting Middleware&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Laravel includes a built-in &lt;code&gt;throttle&lt;/code&gt; middleware that prevents users from making too many requests in a given time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;Route::middleware(['throttle:60,1'])-&amp;gt;group(function () {
    Route::get('/api/data', [ApiController::class, 'index']);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;60&lt;/code&gt; = Number of allowed requests&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt; = Time period in minutes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a client exceeds this, Laravel automatically responds with &lt;code&gt;HTTP 429 Too Many Requests&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can also create custom rate limits for different routes using the &lt;strong&gt;RateLimiter&lt;/strong&gt; facade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function ($request) {
    return Limit::perMinute(30)-&amp;gt;by($request-&amp;gt;ip());
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows per-IP or per-user limits — offering more flexibility.&lt;/p&gt;
&lt;hr&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Start with generous limits and tighten them as you observe real traffic. Too strict limits can frustrate legitimate users.&lt;/span&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-use-caching-and-queues&quot;&gt;&lt;strong&gt;2. Use Caching and Queues&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Caching reduces load on your application by avoiding repetitive queries.&lt;br&gt;
Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;$posts = Cache::remember('posts', 3600, function () {
    return Post::all();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also, using &lt;strong&gt;queues&lt;/strong&gt; (via Laravel Horizon or Redis) prevents slow API responses by offloading heavy tasks to background workers.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-log-and-detect-suspicious-patterns&quot;&gt;&lt;strong&gt;3. Log and Detect Suspicious Patterns&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;You can create middleware to track IPs sending too many requests or unusual payloads.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;public function handle($request, Closure $next)
{
    $ip = $request-&amp;gt;ip();
    Log::info(&quot;Request from IP: $ip&quot;);
    return $next($request);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Combine this with monitoring tools like &lt;strong&gt;Laravel Telescope&lt;/strong&gt; to visualize unusual behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;server-level-protection&quot;&gt;Server-Level Protection&lt;/h3&gt;
&lt;p&gt;Even the best Laravel code can’t defend against everything.&lt;br&gt;
Let’s strengthen things on the &lt;strong&gt;server side&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;1-use-a-firewall&quot;&gt;&lt;strong&gt;1. Use a Firewall&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Set up a firewall (like &lt;strong&gt;UFW&lt;/strong&gt; or &lt;strong&gt;iptables&lt;/strong&gt;) to allow only specific ports and IP ranges.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo ufw allow 22/tcp
sudo ufw allow 80,443/tcp
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures only HTTP(S) and SSH traffic gets through.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-install-fail2ban&quot;&gt;&lt;strong&gt;2. Install Fail2Ban&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Fail2Ban automatically bans IPs that show malicious behavior or too many failed requests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt install fail2ban
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure it to watch Nginx or Apache logs for repeated hits.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-add-nginx-rate-limiting&quot;&gt;&lt;strong&gt;3. Add Nginx Rate Limiting&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;At the reverse proxy layer, you can limit the number of requests per IP before they even reach Laravel.&lt;/p&gt;
&lt;p&gt;Example Nginx config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-nginx&quot;&gt;limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    location / {
        limit_req zone=api_limit burst=20 nodelay;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows 10 requests per second with a small burst tolerance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-infrastructure-level-protection&quot;&gt;☁️ Infrastructure-Level Protection&lt;/h3&gt;
&lt;p&gt;To handle larger-scale attacks, combine Laravel and server-side protections with &lt;strong&gt;infrastructure solutions&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloudflare / AWS Shield:&lt;/strong&gt; Blocks suspicious traffic before it reaches your server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load Balancer:&lt;/strong&gt; Distributes requests across multiple instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SSL (HTTPS):&lt;/strong&gt; Encrypts traffic, making data tampering harder.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatic Scaling:&lt;/strong&gt; In cloud environments, automatically spin up more instances during high load.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;monitoring-and-alerts&quot;&gt;Monitoring and Alerts&lt;/h3&gt;
&lt;p&gt;Detection is half the battle.&lt;br&gt;
Use monitoring tools like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Grafana + Prometheus&lt;/strong&gt; for real-time server metrics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laravel Telescope&lt;/strong&gt; for application-level tracking.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Alerts:&lt;/strong&gt; Send a Telegram or Slack message when request rates spike.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;*/5 * * * * /usr/bin/php /var/www/html/artisan schedule:run &amp;gt;&amp;gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Set up a cron job to monitor request logs and notify you of anomalies.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-conclusion-1&quot;&gt;✅ Conclusion&lt;/h3&gt;
&lt;p&gt;No single defense is bulletproof, but &lt;strong&gt;layered protection&lt;/strong&gt; is your best bet.&lt;br&gt;
Laravel’s rate limiting helps filter abuse, while server and infrastructure layers handle more serious floods.&lt;/p&gt;
&lt;p&gt;By combining these strategies — &lt;strong&gt;middleware, firewalls, monitoring, and CDNs&lt;/strong&gt; — your Laravel API will be resilient, stable, and ready for real-world traffic surges.&lt;/p&gt;
&lt;hr&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Pro-Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Don’t wait for an attack to happen. Simulate high traffic, monitor performance, and tune your defenses early.&lt;/span&gt;
&lt;/div&gt;
</description>
<category>Configuration</category>
<guid isPermaLink="true">https://coderlegion.com/6393/protecting-your-laravel-api-from-ddos-attacks-best-practices</guid>
<pubDate>Mon, 13 Oct 2025 21:02:50 +0000</pubDate>
</item>
</channel>
</rss>