<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Denis Skripchenko</title>
    <description>The latest articles on DEV Community by Denis Skripchenko (@dskripchenko).</description>
    <link>https://dev.to/dskripchenko</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3978750%2F35cd4295-ac6a-4488-99a9-81608f3c1252.jpeg</url>
      <title>DEV Community: Denis Skripchenko</title>
      <link>https://dev.to/dskripchenko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dskripchenko"/>
    <language>en</language>
    <item>
      <title>Migrating from mpdf: an MIT PDF toolkit with no GPL friction</title>
      <dc:creator>Denis Skripchenko</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:57:39 +0000</pubDate>
      <link>https://dev.to/dskripchenko/migrating-from-mpdf-an-mit-pdf-toolkit-with-no-gpl-friction-1o9p</link>
      <guid>https://dev.to/dskripchenko/migrating-from-mpdf-an-mit-pdf-toolkit-with-no-gpl-friction-1o9p</guid>
      <description>&lt;p&gt;Every PHP shop that ships proprietary software eventually hits the same&lt;br&gt;
wall: the de-facto standard HTML→PDF library, mpdf, is GPL-2.0-only. That&lt;br&gt;
means bundling it into an OEM product, an on-premise installer, or any&lt;br&gt;
closed-source codebase requires either shipping your product under GPL or&lt;br&gt;
negotiating a commercial license. I hit that wall often enough that I&lt;br&gt;
built a replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;php-pdf&lt;/strong&gt; (&lt;a href="https://github.com/dskripchenko/php-pdf" rel="noopener noreferrer"&gt;github.com/dskripchenko/php-pdf&lt;/a&gt;)&lt;br&gt;
is a pure-PHP, MIT-licensed PDF toolkit: HTML/CSS input, fluent builders,&lt;br&gt;
TTF embedding with subsetting, barcodes, charts, AcroForm, PKCS#7&lt;br&gt;
signing, PDF/A and PDF/X — plus reading and merging existing PDFs.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Sounds too good" — so prove it
&lt;/h2&gt;

&lt;p&gt;A two-week-old library claiming production crypto and PDF/A conformance&lt;br&gt;
deserves skepticism. That's why every push to main runs an independent&lt;br&gt;
proof chain in CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PDF/A&lt;/strong&gt; references (1a/1b/2b/2u/3b) validated with &lt;strong&gt;veraPDF&lt;/strong&gt; — the
industry-standard validator, not our own code;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF/X&lt;/strong&gt; structure checked byte-level plus a Ghostscript render pass;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;signatures&lt;/strong&gt; verified by the OpenSSL CLI against the exact
&lt;code&gt;/ByteRange&lt;/code&gt; bytes — and a tampered byte must break verification;&lt;/li&gt;
&lt;li&gt;renders diffed against &lt;strong&gt;golden images&lt;/strong&gt; with poppler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reports are public artifacts on every run:&lt;br&gt;
&lt;a href="https://github.com/dskripchenko/php-pdf/blob/main/docs/en/CONFORMANCE.md" rel="noopener noreferrer"&gt;conformance page&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The migration is mechanical
&lt;/h2&gt;

&lt;p&gt;For the typical mpdf call site, swap one import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Mpdf\Mpdf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'A4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'margin_left'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;WriteHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\Dskripchenko\PhpPdf\Compat\Mpdf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'A4'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'margin_left'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;WriteHTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$mpdf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'invoice.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compat facade covers &lt;code&gt;WriteHTML&lt;/code&gt;/&lt;code&gt;AddPage&lt;/code&gt;/&lt;code&gt;Output&lt;/code&gt; (all four&lt;br&gt;
destinations), metadata setters and the common config keys — and it's&lt;br&gt;
exercised by the test suite, so the examples in the&lt;br&gt;
&lt;a href="https://github.com/dskripchenko/php-pdf/blob/main/docs/en/MIGRATION-FROM-MPDF.md" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt;&lt;br&gt;
are tested code, not aspiration. The guide also has the full native-API&lt;br&gt;
mapping table and a find/replace cheat-sheet for &lt;code&gt;Destination::*&lt;/code&gt;&lt;br&gt;
constants.&lt;/p&gt;

&lt;p&gt;What the facade deliberately does NOT do: mpdf-specific HTML extensions&lt;br&gt;
(&lt;code&gt;&amp;lt;pagebreak&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;barcode&amp;gt;&lt;/code&gt;), &lt;code&gt;SetHeader&lt;/code&gt; shortcodes, font-config arrays.&lt;br&gt;
Those map to the native API, which is better at all of them (real block&lt;br&gt;
elements for headers/footers, a first-class &lt;code&gt;Barcode&lt;/code&gt; element with 16&lt;br&gt;
formats, engine-level font config).&lt;/p&gt;

&lt;h2&gt;
  
  
  And it's faster
&lt;/h2&gt;

&lt;p&gt;Reproducible benchmark (isolated subprocesses, medians, pinned competitor&lt;br&gt;
versions — &lt;code&gt;composer bench&lt;/code&gt; from a clean clone re-runs everything):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;php-pdf&lt;/th&gt;
&lt;th&gt;mpdf 8.3.1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML → PDF article (~5 pages)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.0 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;64.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100-page invoice&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;576 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2640 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image grid (20 pages)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;36.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full methodology and all four competitors:&lt;br&gt;
&lt;a href="https://github.com/dskripchenko/php-pdf/blob/main/docs/en/BENCHMARKS.md" rel="noopener noreferrer"&gt;BENCHMARKS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The HTML/CSS subset is narrower than mpdf's in places (&lt;code&gt;@page&lt;/code&gt; rules
map to a &lt;code&gt;PageSetup&lt;/code&gt; object instead).&lt;/li&gt;
&lt;li&gt;Nothing auto-embeds fonts: base-14 covers Latin; Cyrillic/Arabic/CJK
need a TTF passed to the engine (it is subset automatically).&lt;/li&gt;
&lt;li&gt;Public-key encryption (&lt;code&gt;/PubSec&lt;/code&gt;) is intentionally unimplemented — we
won't ship crypto we can't test against real fixtures.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel users: there's a thin bridge —&lt;br&gt;
&lt;a href="https://github.com/dskripchenko/laravel-php-pdf" rel="noopener noreferrer"&gt;dskripchenko/laravel-php-pdf&lt;/a&gt;&lt;br&gt;
with a &lt;code&gt;Pdf&lt;/code&gt; facade and a &lt;code&gt;response()-&amp;gt;pdf()&lt;/code&gt; macro.&lt;/p&gt;

&lt;p&gt;If GPL friction has ever made you postpone a PDF feature, give it a try:&lt;br&gt;
&lt;code&gt;composer require dskripchenko/php-pdf&lt;/code&gt;. Issues and failing PDFs from&lt;br&gt;
real-world producers are the most valuable thing you can send my way.&lt;/p&gt;

</description>
      <category>php</category>
      <category>pdf</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Generate, read, and merge PDFs in PHP — no GPL, no FPDI</title>
      <dc:creator>Denis Skripchenko</dc:creator>
      <pubDate>Mon, 06 Jul 2026 11:14:39 +0000</pubDate>
      <link>https://dev.to/dskripchenko/generate-read-and-merge-pdfs-in-php-no-gpl-no-fpdi-16l6</link>
      <guid>https://dev.to/dskripchenko/generate-read-and-merge-pdfs-in-php-no-gpl-no-fpdi-16l6</guid>
      <description>&lt;p&gt;Every PHP project eventually needs a PDF — an invoice, a ticket, a report, a&lt;br&gt;
signed contract. And every time, you hit the same two walls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generating&lt;/strong&gt; PDFs usually means &lt;a href="https://github.com/mpdf/mpdf" rel="noopener noreferrer"&gt;&lt;code&gt;mpdf&lt;/code&gt;&lt;/a&gt;,
which is &lt;strong&gt;GPL-2.0&lt;/strong&gt;. Fine for internal tools, a licensing headache the
moment you ship it inside a commercial, OEM, or on-premise product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merging or importing&lt;/strong&gt; existing PDFs (stamp a watermark, prepend a cover,
overlay a letterhead) means &lt;a href="https://www.setasign.com/products/fpdi/about/" rel="noopener noreferrer"&gt;FPDI&lt;/a&gt;
— and FPDI's PDF-parsing add-on is &lt;strong&gt;proprietary&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/dskripchenko/php-pdf" rel="noopener noreferrer"&gt;&lt;code&gt;dskripchenko/php-pdf&lt;/code&gt;&lt;/a&gt; is a&lt;br&gt;
&lt;strong&gt;pure-PHP, MIT-licensed&lt;/strong&gt; toolkit that does both — generate &lt;em&gt;and&lt;/em&gt; read/merge —&lt;br&gt;
with no native binaries and no add-ons. It runs anywhere your Laravel app runs&lt;br&gt;
(only &lt;code&gt;ext-mbstring&lt;/code&gt;, &lt;code&gt;ext-dom&lt;/code&gt;, &lt;code&gt;ext-zlib&lt;/code&gt;; &lt;code&gt;ext-openssl&lt;/code&gt; for AES/signing).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require dskripchenko/php-pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Generating a PDF
&lt;/h2&gt;

&lt;p&gt;Three entry points, from highest to lowest level. The quickest is HTML/CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;HTML
&amp;lt;h1&amp;gt;Invoice #1042&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Customer: &amp;lt;strong&amp;gt;Acme Corp&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;table&amp;gt;
  &amp;lt;thead&amp;gt;&amp;lt;tr&amp;gt;&amp;lt;th&amp;gt;Item&amp;lt;/th&amp;gt;&amp;lt;th&amp;gt;Price&amp;lt;/th&amp;gt;&amp;lt;/tr&amp;gt;&amp;lt;/thead&amp;gt;
  &amp;lt;tbody&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Consulting&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;$1,200.00&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
    &amp;lt;tr&amp;gt;&amp;lt;td&amp;gt;Hosting&amp;lt;/td&amp;gt;&amp;lt;td&amp;gt;$99.00&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;
  &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
HTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a fluent &lt;code&gt;DocumentBuilder&lt;/code&gt; and a low-level page API, plus 16&lt;br&gt;
barcode formats, 8 chart types, TTF embedding with kerning, AcroForm fields,&lt;br&gt;
PKCS#7 signing, and PDF/A &amp;amp; PDF/X conformance. But generation isn't the news&lt;br&gt;
here — &lt;strong&gt;reading and merging is.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  New in 1.1: reading &amp;amp; merging
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Merge and reorder pages
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PdfMerger&lt;/code&gt; concatenates whole or selected pages from any mix of files and&lt;br&gt;
in-memory bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Dskripchenko\PhpPdf\Pdf\Merge\&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cover.pdf'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;// the bytes we just generated&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'terms.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'packet.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Page annotations and bookmarks are carried over by default, with internal links&lt;br&gt;
and named destinations remapped to the new pages.&lt;/p&gt;
&lt;h3&gt;
  
  
  Stamp a watermark or letterhead
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stamp()&lt;/code&gt; draws a source page over the output pages as a reusable Form XObject —&lt;br&gt;
perfect for a "DRAFT" watermark or a corporate letterhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Dskripchenko\PhpPdf\Pdf\Merge\&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Placement&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contract.pdf'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'draft-watermark.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Placement&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'contract-draft.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Placement&lt;/code&gt; gives you &lt;code&gt;fit()&lt;/code&gt; (scale to page, centered), &lt;code&gt;stretch()&lt;/code&gt;, or&lt;br&gt;
&lt;code&gt;at($x, $y, $scale)&lt;/code&gt; for pixel-precise positioning.&lt;/p&gt;
&lt;h3&gt;
  
  
  FPDI-style: drop an imported page into a generated document
&lt;/h3&gt;

&lt;p&gt;Sometimes you want to combine an &lt;em&gt;existing&lt;/em&gt; PDF with &lt;em&gt;freshly generated&lt;/em&gt;&lt;br&gt;
content — e.g. print "PAID" and a QR code onto a scanned letterhead. Import the&lt;br&gt;
page as a Form XObject and draw over or under it with the normal API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Pdf\Document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Pdf\StandardFont&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Pdf\Merge\PageImporter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Pdf\Reader\ReaderDocument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReaderDocument&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'letterhead.pdf'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageImporter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$doc&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customDimensionsPt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nv"&gt;$form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageImporter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;intoDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;useFormXObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                 &lt;span class="c1"&gt;// imported page as background&lt;/span&gt;
&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;showText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'PAID'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;750&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StandardFont&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Helvetica&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// your content on top&lt;/span&gt;

&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'paid.pdf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rotation and crop boxes are handled automatically; the imported page's fonts and&lt;br&gt;
images are copied verbatim.&lt;/p&gt;
&lt;h2&gt;
  
  
  A Laravel example: batch invoices into one file
&lt;/h2&gt;

&lt;p&gt;A common real-world job: generate a PDF per invoice, then merge the batch into a&lt;br&gt;
single file for download or email. In a queued job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Contracts\Queue\ShouldQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Document&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;Dskripchenko\PhpPdf\Pdf\Merge\&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BuildInvoiceBatch&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ShouldQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/** @param \App\Models\Invoice[] $invoices */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$invoices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$outputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$merger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PdfMerger&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromHtml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pdf.invoice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'invoice'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$invoice&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="nv"&gt;$merger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PdfSource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pdf&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$merger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One Blade view for the invoice, one &lt;code&gt;PdfMerger&lt;/code&gt; for the batch — no shell-outs,&lt;br&gt;
no wkhtmltopdf binary, no GPL.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading, too
&lt;/h2&gt;

&lt;p&gt;Need to inspect an incoming PDF before merging? &lt;code&gt;ReaderDocument&lt;/code&gt; parses it —&lt;br&gt;
classic and stream cross-references, object streams, corrupt-xref recovery, and&lt;br&gt;
decryption (RC4 / AES-128 / AES-256, user &lt;em&gt;and&lt;/em&gt; owner password):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\PhpPdf\Pdf\Reader\ReaderDocument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ReaderDocument&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'protected.pdf'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pageCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"%.0f × %.0f pt&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;width&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;height&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Encrypted input is decrypted transparently and re-emitted unencrypted in the&lt;br&gt;
merged output.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it handles (and doesn't, yet)
&lt;/h2&gt;

&lt;p&gt;The reader is validated against a third-party corpus — LibreOffice, Google Docs,&lt;br&gt;
Ghostscript, pdfTeX, ImageMagick, Qt/pdfkit, FPDF2, pypdf — with CI on PHP&lt;br&gt;
8.2 / 8.3 / 8.4 and a PHPStan level-6 gate. v1 merging carries content, images,&lt;br&gt;
fonts, annotations, and bookmarks; it does &lt;strong&gt;not&lt;/strong&gt; yet carry AcroForm field&lt;br&gt;
trees or structure tags. It's all documented in the&lt;br&gt;
&lt;a href="https://github.com/dskripchenko/php-pdf/blob/main/docs/en/MERGE.md" rel="noopener noreferrer"&gt;reading &amp;amp; merging guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/dskripchenko/php-pdf" rel="noopener noreferrer"&gt;https://github.com/dskripchenko/php-pdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packagist:&lt;/strong&gt; &lt;code&gt;composer require dskripchenko/php-pdf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MIT-licensed, PHP 8.2+, pure PHP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your PHP app generates PDFs and you've been paying the mpdf/FPDI licensing tax&lt;br&gt;
— or shelling out to a native binary — this might be worth a look. Feedback and&lt;br&gt;
issues welcome.&lt;/p&gt;

</description>
      <category>php</category>
      <category>opensource</category>
      <category>pdf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build a versioned Laravel API with auto-generated OpenAPI docs in 10 minutes</title>
      <dc:creator>Denis Skripchenko</dc:creator>
      <pubDate>Thu, 11 Jun 2026 06:12:56 +0000</pubDate>
      <link>https://dev.to/dskripchenko/build-a-versioned-laravel-api-with-auto-generated-openapi-docs-in-10-minutes-2d19</link>
      <guid>https://dev.to/dskripchenko/build-a-versioned-laravel-api-with-auto-generated-openapi-docs-in-10-minutes-2d19</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — We'll install &lt;a href="https://github.com/dskripchenko/laravel-api" rel="noopener noreferrer"&gt;&lt;code&gt;dskripchenko/laravel-api&lt;/code&gt;&lt;/a&gt;, write one controller, and end up with a versioned API (&lt;code&gt;/api/v1/...&lt;/code&gt;) &lt;strong&gt;and&lt;/strong&gt; interactive OpenAPI 3.0 docs at &lt;code&gt;/api/doc&lt;/code&gt; — generated from the docblock you'd write anyway. Then we'll ship a &lt;code&gt;v2&lt;/code&gt; without copy-pasting a single controller.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Two things rot in every growing Laravel API:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Versioning.&lt;/strong&gt; &lt;code&gt;v1&lt;/code&gt; ships, then &lt;code&gt;v2&lt;/code&gt; needs to change three endpoints but keep the other twenty. You either copy-paste a &lt;code&gt;V2&lt;/code&gt; folder (and now bugfixes live in two places) or bolt &lt;code&gt;if ($version === 2)&lt;/code&gt; branches into your controllers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs.&lt;/strong&gt; The OpenAPI spec drifts from the code the moment you merge. Annotation libraries (&lt;code&gt;#[OA\Get(...)]&lt;/code&gt;, giant YAML files) ask you to describe your API &lt;em&gt;twice&lt;/em&gt; — once in code, once in attributes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This package's bet: &lt;strong&gt;your controller already describes itself&lt;/strong&gt;. The method name, the request fields, the response shape — write them once, as a normal PHPDoc, and let the package derive routes &lt;em&gt;and&lt;/em&gt; docs from it. Versioning becomes plain PHP inheritance.&lt;/p&gt;

&lt;p&gt;Let's build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'll build
&lt;/h2&gt;

&lt;p&gt;A tiny &lt;code&gt;tasks&lt;/code&gt; API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/task/list&lt;/code&gt; — list tasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /api/v1/task/create&lt;/code&gt; — create one&lt;/li&gt;
&lt;li&gt;interactive docs at &lt;code&gt;GET /api/doc&lt;/code&gt; (raw spec per version at &lt;code&gt;/api/doc/{version}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;then a &lt;code&gt;v2&lt;/code&gt; that adds an endpoint &lt;strong&gt;without touching v1&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: ~4 small files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0 — Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require dskripchenko/laravel-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Publish the config (optional, but handy to see the knobs):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan vendor:publish &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;laravel-api-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// config/laravel-api.php&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'prefix'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'api'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                            &lt;span class="c1"&gt;// → /api/...&lt;/span&gt;
    &lt;span class="s1"&gt;'uri_pattern'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'{version}/{controller}/{action}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'available_methods'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'get'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'put'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'patch'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'delete'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'openapi_path'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'public/openapi'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'doc_middleware'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;                               &lt;span class="c1"&gt;// lock down /api/doc here&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1 — Write a controller
&lt;/h2&gt;

&lt;p&gt;Nothing exotic — it extends the package's &lt;code&gt;ApiController&lt;/code&gt;, which gives you response helpers (&lt;code&gt;success()&lt;/code&gt;, &lt;code&gt;error()&lt;/code&gt;, &lt;code&gt;validationError()&lt;/code&gt;, &lt;code&gt;created()&lt;/code&gt;, &lt;code&gt;noContent()&lt;/code&gt;, &lt;code&gt;notFound()&lt;/code&gt;). The &lt;strong&gt;docblock is the documentation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\LaravelApi\Controllers\ApiController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\JsonResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ApiController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * List tasks
     * Returns the current user's tasks.
     *
     * @input integer ?$page Page number
     * @input string $status Filter by status [open,done]
     *
     * @output integer $id Task id
     * @output string $title Task title
     * @output string $status Current status
     *
     * @return JsonResponse
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Pack for Vietnam'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'done'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Write this article'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'open'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/**
     * Create a task
     *
     * @input string $title Task title
     * @input string ?$status Initial status [open,done]
     *
     * @output integer $id New task id
     * @output string $title Task title
     *
     * @return JsonResponse
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;created&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few docblock conventions worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;?$page&lt;/code&gt; → optional field.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;string $status ... [open,done]&lt;/code&gt; → the bracketed list becomes an &lt;strong&gt;enum&lt;/strong&gt; in the spec.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@input integer(int64) $id&lt;/code&gt; / &lt;code&gt;@input string(email) $email&lt;/code&gt; → type &lt;strong&gt;with format&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@input file $avatar&lt;/code&gt; → file upload; &lt;code&gt;@input @User $user&lt;/code&gt; → &lt;code&gt;$ref&lt;/code&gt; to a component schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every response is wrapped in a consistent envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Errors (thrown &lt;code&gt;ApiException&lt;/code&gt; or &lt;code&gt;$this-&amp;gt;error()&lt;/code&gt;) come back as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errorKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — Wire up the version, the module, the provider
&lt;/h2&gt;

&lt;p&gt;Three small classes. &lt;strong&gt;This is the whole routing layer&lt;/strong&gt; — no &lt;code&gt;routes/api.php&lt;/code&gt; entries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/Api/V1/Api.php — defines what v1 exposes&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Api\V1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\LaravelApi\Components\BaseApi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Api\V1\Controllers\TaskController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Api&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseApi&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getMethods&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'controllers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'task'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;                          &lt;span class="c1"&gt;// → /api/v1/task/{action}&lt;/span&gt;
                    &lt;span class="s1"&gt;'controller'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;TaskController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s1"&gt;'actions'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'list'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'create'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/Api/ApiModule.php — maps version strings to Api classes&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\LaravelApi\Components\BaseModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiModule&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseModule&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getApiVersionList&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'v1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Api\V1\Api&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/Providers/ApiServiceProvider.php — plugs the module in&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Dskripchenko\LaravelApi\Providers\ApiServiceProvider&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;BaseApiServiceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Api\ApiModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApiServiceProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseApiServiceProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getApiModule&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;ApiModule&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiModule&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the provider (Laravel 11/12/13 — &lt;code&gt;bootstrap/providers.php&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;App\Providers\AppServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;App\Providers\ApiServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 👈&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The base provider registers the routes and the &lt;code&gt;/api/doc&lt;/code&gt; endpoint for you on boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Call it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/api/v1/task/list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pack for Vietnam"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"done"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write this article"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Actions are &lt;code&gt;POST&lt;/code&gt; by default. Want &lt;code&gt;GET&lt;/code&gt;? Declare it per action:&lt;br&gt;
&lt;code&gt;'list' =&amp;gt; ['action' =&amp;gt; 'list', 'method' =&amp;gt; ['get']]&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4 — The payoff: free OpenAPI docs
&lt;/h2&gt;

&lt;p&gt;Open &lt;strong&gt;&lt;code&gt;GET /api/doc&lt;/code&gt;&lt;/strong&gt; in a browser. You don't get a raw JSON file — you get a ready-to-use &lt;strong&gt;interactive API reference&lt;/strong&gt; (rendered with &lt;a href="https://github.com/scalar/scalar" rel="noopener noreferrer"&gt;Scalar&lt;/a&gt;), with a version switcher across &lt;code&gt;v1&lt;/code&gt;, &lt;code&gt;v2&lt;/code&gt;, … already wired up. The package walked your controllers, read those docblocks, and produced a complete OpenAPI 3.0 document — parameters, enums, response schemas, the lot. These docs &lt;strong&gt;can't drift&lt;/strong&gt;, because they &lt;em&gt;are&lt;/em&gt; the code.&lt;/p&gt;

&lt;p&gt;Need the raw spec for CI, a client generator, or your own Redoc/Stoplight setup? Each version is served as JSON at &lt;strong&gt;&lt;code&gt;GET /api/doc/{version}&lt;/code&gt;&lt;/strong&gt; (e.g. &lt;code&gt;/api/doc/v1&lt;/code&gt;) — no &lt;code&gt;storage:link&lt;/code&gt;, no build step.&lt;/p&gt;

&lt;p&gt;Need TypeScript clients? There's a generator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan api:generate-types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and an exporter for Postman / HTTP Client / Markdown / cURL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan api:export &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postman
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — Versioning without copy-paste
&lt;/h2&gt;

&lt;p&gt;Here's the part that usually hurts. &lt;code&gt;v2&lt;/code&gt; should add an &lt;code&gt;archive&lt;/code&gt; endpoint — but leave &lt;code&gt;v1&lt;/code&gt; untouched. You &lt;strong&gt;extend&lt;/strong&gt; the previous version, at both the controller and the &lt;code&gt;Api&lt;/code&gt; level.&lt;/p&gt;

&lt;p&gt;The v2 controller inherits every v1 action and adds the new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/Api/V2/Controllers/TaskController.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Api\V2\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Api\V1\Controllers\TaskController&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;V1TaskController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\JsonResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;V1TaskController&lt;/span&gt;   &lt;span class="c1"&gt;// inherits list() and create()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * Archive a task
     *
     * @input integer $id Task id
     *
     * @output boolean $archived Whether it was archived
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'archived'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the v2 &lt;code&gt;Api&lt;/code&gt; inherits every v1 action, swapping in the new controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="c1"&gt;// app/Api/V2/Api.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Api\V2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Api\V1\Api&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;V1Api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Api\V2\Controllers\TaskController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Api&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;V1Api&lt;/span&gt;          &lt;span class="c1"&gt;// inherits every v1 action…&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getMethods&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'controllers'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'task'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                    &lt;span class="s1"&gt;'controller'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;TaskController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// …override just this one&lt;/span&gt;
                    &lt;span class="s1"&gt;'actions'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="s1"&gt;'list'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'create'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s1"&gt;'archive'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// add a brand-new action&lt;/span&gt;
                        &lt;span class="s1"&gt;'legacyExport'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// → false disables an action (e.g. one inherited from v1)&lt;/span&gt;
                    &lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/Api/ApiModule.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getApiVersionList&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'v1'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Api\V1\Api&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'v2'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;\App\Api\V2\Api&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;/api/v2/task/...&lt;/code&gt; is live, &lt;code&gt;v2&lt;/code&gt;'s docs appear automatically, and &lt;strong&gt;v1 never changed&lt;/strong&gt;. Bugfix in a shared action? Fix it once in the base class. Need a clean break? Override the controller. Need to kill an endpoint in the new version? Set the action key to &lt;code&gt;false&lt;/code&gt; (as with &lt;code&gt;'legacyExport' =&amp;gt; false&lt;/code&gt; above).&lt;/p&gt;

&lt;p&gt;Middleware cascades the same way — global → controller → action, with &lt;code&gt;exclude-middleware&lt;/code&gt; / &lt;code&gt;exclude-all-middleware&lt;/code&gt; escape hatches at each level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this approach
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Annotation libs (&lt;code&gt;#[OA\...]&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;This package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Describe API&lt;/td&gt;
&lt;td&gt;in code &lt;strong&gt;and&lt;/strong&gt; in attributes&lt;/td&gt;
&lt;td&gt;once, in the PHPDoc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Versioning&lt;/td&gt;
&lt;td&gt;manual folders / &lt;code&gt;if&lt;/code&gt; branches&lt;/td&gt;
&lt;td&gt;PHP inheritance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs drift&lt;/td&gt;
&lt;td&gt;possible (separate source)&lt;/td&gt;
&lt;td&gt;impossible (derived)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Routes&lt;/td&gt;
&lt;td&gt;hand-written&lt;/td&gt;
&lt;td&gt;derived from &lt;code&gt;getMethods()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It won't replace a full framework for every team — if you love attribute-driven specs, you'll miss them here. But if you've ever grep'd a controller wondering whether the docs still match it, this removes the question entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require dskripchenko/laravel-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;⭐ Repo &amp;amp; full docs: &lt;a href="https://github.com/dskripchenko/laravel-api" rel="noopener noreferrer"&gt;https://github.com/dskripchenko/laravel-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 Packagist: &lt;a href="https://packagist.org/packages/dskripchenko/laravel-api" rel="noopener noreferrer"&gt;https://packagist.org/packages/dskripchenko/laravel-api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I maintain it — issues, ideas, and "this broke on my setup" reports are all welcome. What would you want a versioning-first API package to do that yours doesn't?&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>api</category>
      <category>openapi</category>
    </item>
  </channel>
</rss>
