<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Robin</title>
    <description>The latest articles on Forem by Robin (@zappzerapp).</description>
    <link>https://forem.com/zappzerapp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1093011%2F616dfd8e-f467-41fe-b20b-d1372bb3a459.jpeg</url>
      <title>Forem: Robin</title>
      <link>https://forem.com/zappzerapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zappzerapp"/>
    <language>en</language>
    <item>
      <title>From 400-Line Import Controllers to 20-Line Configs in Laravel</title>
      <dc:creator>Robin</dc:creator>
      <pubDate>Wed, 04 Feb 2026 22:35:15 +0000</pubDate>
      <link>https://forem.com/zappzerapp/from-400-line-import-controllers-to-20-line-configs-in-laravel-p99</link>
      <guid>https://forem.com/zappzerapp/from-400-line-import-controllers-to-20-line-configs-in-laravel-p99</guid>
      <description>&lt;h1&gt;
  
  
  The "Import Nightmares" We All Know
&lt;/h1&gt;

&lt;p&gt;If you've built business applications with Laravel, you've definitely received this ticket:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"As an admin, I need to upload a CSV with 50,000 products to update our stock levels."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've seen this request at least a dozen times across different projects. It sounds simple. So you grab a CSV parser, maybe &lt;code&gt;league/csv&lt;/code&gt; or &lt;code&gt;maatwebsite/excel&lt;/code&gt;, and start writing a Controller.&lt;/p&gt;

&lt;p&gt;Ten minutes later, you're deep in the weeds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How do I validate row 49,000 without crashing memory?"&lt;/li&gt;
&lt;li&gt;"The client calls the column 'E-Mail', but sometimes 'Email Address'."&lt;/li&gt;
&lt;li&gt;"I need to find the Category ID by name, but create it if it doesn't exist."&lt;/li&gt;
&lt;li&gt;"The client wants a 'Dry Run' to see errors before committing."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your Controller becomes a 400-line monster of &lt;code&gt;while&lt;/code&gt; loops, &lt;code&gt;try-catch&lt;/code&gt; blocks, and manual validation logic. It's hard to test, hard to read, and terrifying to refactor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There has to be a better way.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Introducing Laravel Ingest
&lt;/h1&gt;

&lt;p&gt;I built &lt;a href="https://github.com/zappzerapp/laravel-ingest" rel="noopener noreferrer"&gt;&lt;strong&gt;Laravel Ingest&lt;/strong&gt;&lt;/a&gt; to stop this madness.&lt;/p&gt;

&lt;p&gt;Laravel Ingest is a &lt;strong&gt;configuration-driven framework&lt;/strong&gt; for data imports. Instead of writing procedural scripts, you define &lt;strong&gt;what&lt;/strong&gt; you want to import, and the package handles the &lt;strong&gt;how&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It takes care of the dirty work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Streaming &amp;amp; Queues:&lt;/strong&gt; Zero memory issues, whether 100 or 1 million rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping &amp;amp; Transformation:&lt;/strong&gt; Fluent API to map CSV columns to Eloquent attributes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships:&lt;/strong&gt; Automatically resolves &lt;code&gt;BelongsTo&lt;/code&gt; and &lt;code&gt;BelongsToMany&lt;/code&gt; relations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dry Runs:&lt;/strong&gt; Simulate imports to find errors without touching the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API &amp;amp; CLI:&lt;/strong&gt; Auto-generated endpoints and Artisan commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PHP 8.3+&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laravel 10, 11, or 12&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Let's say we want to import Users. Instead of a Controller, we create an &lt;strong&gt;Importer Class&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Declarative Config
&lt;/h3&gt;

&lt;p&gt;This is where the magic happens. Look how readable this is:&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Ingest&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\Models\User&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;LaravelIngest\Contracts\IngestDefinition&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;LaravelIngest\IngestConfig&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;LaravelIngest\Enums\SourceType&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;LaravelIngest\Enums\DuplicateStrategy&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;UserImporter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;IngestDefinition&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;getConfig&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;IngestConfig&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;IngestConfig&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&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="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fromSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SourceType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UPLOAD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Identify records by email&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// If email exists, update the record&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onDuplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DuplicateStrategy&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Map CSV 'Full Name' to DB 'name'&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Full Name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Map CSV 'E-Mail' (or 'Email') to DB 'email'&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'E-Mail'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Email'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Transform 'yes/no' string to boolean&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;mapAndTransform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Is Admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$val&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Use Laravel Validation rules per row&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
                &lt;span class="s1"&gt;'Full Name'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|min:3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s1"&gt;'E-Mail'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|email'&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;h3&gt;
  
  
  2. Registering the Importer
&lt;/h3&gt;

&lt;p&gt;In your &lt;code&gt;AppServiceProvider&lt;/code&gt;, simply tag 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="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nc"&gt;UserImporter&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;'ingest.definition'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Running the Import
&lt;/h3&gt;

&lt;p&gt;You don't need to write a Controller for uploads. Laravel Ingest automatically provides API endpoints and Artisan commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Via CLI (great for cronjobs or S3 imports):&lt;/strong&gt;&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 ingest:run user-importer &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;users.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Via API (for your frontend):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POST /api/v1/ingest/upload/user-importer
Body: &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;@users.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Killer Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Handling Relationships Is Finally Easy
&lt;/h3&gt;

&lt;p&gt;Usually, importing related data (like assigning a Product to a Category by name) requires annoying lookup logic. Ingest does it in one line:&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;// Looks up the Category by 'name'. &lt;/span&gt;
&lt;span class="c1"&gt;// If it doesn't exist, it creates it!&lt;/span&gt;
&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;relate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sourceColumn&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Category Name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'category'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relatedModel&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Category&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="n"&gt;lookupColumn&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;createIfMissing&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Dry Runs Out of the Box
&lt;/h3&gt;

&lt;p&gt;Clients hate it when an import fails halfway through. With Ingest, you can trigger a simulation that runs all validations and transformations but rolls back changes.&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 ingest:run user-importer &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;users.csv &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Error Analysis API
&lt;/h3&gt;

&lt;p&gt;When rows fail, you don't just get a log file. Ingest tracks failed rows in the database and provides an API endpoint to download a &lt;strong&gt;CSV of only the failed rows&lt;/strong&gt;, including error messages. Your users can fix the errors in Excel and re-upload just those rows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;Laravel Ingest stands on the shoulders of giants. It uses &lt;a href="https://github.com/spatie/simple-excel" rel="noopener noreferrer"&gt;&lt;code&gt;spatie/simple-excel&lt;/code&gt;&lt;/a&gt; to stream files line-by-line (keeping memory usage flat) and pushes chunks of rows onto the Laravel Queue.&lt;/p&gt;

&lt;p&gt;This means your application stays responsive, even when importing a 500MB file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Installation is straightforward:&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 zappzerapp/laravel-ingest
php artisan vendor:publish &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"LaravelIngest&lt;/span&gt;&lt;span class="se"&gt;\I&lt;/span&gt;&lt;span class="s2"&gt;ngestServiceProvider"&lt;/span&gt;
php artisan migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/zappzerapp/laravel-ingest" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://zappzerapp.github.io/laravel-ingest/" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/zappzerapp/Laravel-Ingest-Demo" rel="noopener noreferrer"&gt;Demo Project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;I built this package because I was tired of writing the same fragile import code for every project. If you've ever dreaded the words "CSV upload", give Laravel Ingest a try.&lt;/p&gt;

&lt;p&gt;Found it useful? Star the repo on GitHub, open an issue if you have feature requests, or drop a comment below. I'd love to hear how you're using it!&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
