<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Petro Lashyn]]></title><description><![CDATA[Hi! My name is Petro Lashyn. This is my personal journal about Laravel, Software Development and AI. Technical discourse, architectural logs, and research notes.]]></description><link>https://labrodev.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!tdmw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34a63639-3b2a-4ca7-af73-5b23b9973ab5_400x400.png</url><title>Petro Lashyn</title><link>https://labrodev.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sat, 09 May 2026 13:21:20 GMT</lastBuildDate><atom:link href="https://labrodev.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Labro Dev]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[labrodev@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[labrodev@substack.com]]></itunes:email><itunes:name><![CDATA[Petro Lashyn]]></itunes:name></itunes:owner><itunes:author><![CDATA[Petro Lashyn]]></itunes:author><googleplay:owner><![CDATA[labrodev@substack.com]]></googleplay:owner><googleplay:email><![CDATA[labrodev@substack.com]]></googleplay:email><googleplay:author><![CDATA[Petro Lashyn]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why RAG Is Not Only Retrieval-Augmentation-Generation]]></title><description><![CDATA[Three words. Three steps. Turn the query into a vector, find matching text, let the model answer. There&#8217;s much more behind it.]]></description><link>https://labrodev.substack.com/p/why-rag-is-not-only-retrieval-augmentation</link><guid isPermaLink="false">https://labrodev.substack.com/p/why-rag-is-not-only-retrieval-augmentation</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sat, 18 Apr 2026 14:15:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tdmw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34a63639-3b2a-4ca7-af73-5b23b9973ab5_400x400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Three words. Three steps. Turn the query into a vector, find matching text, let the model answer.</p><p>Sounds like a solved problem. And honestly, if you build it this way, your first ten test queries will look great. You&#8217;ll show it to a colleague and feel proud.</p><p>Then someone types &#8220;the bot stopped responding after I configured the webhook&#8221; and your neat three-step system retrieves chunks about webhook setup, chunks about bot response formatting, and chunks about Slack timeout handling. Every chunk ranked well on similarity. None of them contain the actual answer. The answer is hiding in the relationship between a misconfigured webhook endpoint and an error handler that quietly drops incoming messages.</p><p>Vector search located text that looks like the question. The answer slipped right through.</p><p>This is the line between tutorial RAG and production RAG.</p><h3>Five Problems the Acronym Hides</h3><p>I&#8217;ve been running a RAG pipeline in production, and every failure that wasn&#8217;t immediately obvious falls into one of five categories. The three-step model acknowledges none of them.</p><p><strong>1. The classification problem.</strong></p><p>Not every message from a user needs retrieval. Someone writes &#8220;got it, thanks&#8221; and the system launches a full embedding pass, hits the vector store, pulls back documentation chunks tangentially related to gratitude, and composes a paragraph about being happy to assist. Burned tokens, added latency, awkward interaction.</p><p>The first meaningful decision in any pipeline should be: does this input actually need retrieval? Small talk bypasses everything and jumps to generation. A single decision gate. In my system, this saves compute on about a third of all incoming messages.</p><p><strong>2. The ambiguity problem.</strong></p><p>&#8220;How do I configure the integration?&#8221; The product has six integrations. The system converts the query to a vector, retrieves documentation for whichever integration landed closest in embedding space, and answers with total confidence about the wrong one. The user can&#8217;t tell the answer is wrong because the system never hesitated.</p><p>This isn&#8217;t a model problem. It&#8217;s an architecture problem. Before you generate anything, check whether the retrieved documents point to one topic or several. If they point to several, the right response is a clarifying question, not a confident guess.</p><p>I solve this with graph cluster detection. When matched entities land in disconnected clusters &#8212; no path between them within four hops &#8212; the query spans multiple unrelated topics. The system asks the user to narrow it down.</p><p><strong>3. The context overflow problem.</strong></p><p>You retrieve 20 solid chunks. That&#8217;s about 15,000 tokens of context. You feed all of it into the prompt. The model pays attention to the top, pays attention to the bottom, and glazes over everything in between.</p><p>&#8220;Lost-in-the-middle&#8221; is well-studied at this point. Fetching fewer chunks sacrifices recall. The real fix is summarizing the retrieved context before handing it to generation. Compress 15,000 tokens of raw chunks down to 3,000 tokens of distilled summary. Now the model actually reads the whole thing.</p><p>I cache summaries keyed by content hash. Same context bundle hits the cache instead of burning another LLM call. Simple optimization, real savings on repeated questions.</p><p><strong>4. The flat retrieval problem.</strong></p><p>This one matters most.</p><p>Vector similarity works on textual surface. It finds chunks that sound like your query. But knowledge isn&#8217;t flat. It has topology. Causes chain into effects. Prerequisites gate actions. Alternatives fork from shared roots.</p><p>&#8220;What happens when finding X is linked to corrective action Y?&#8221; That&#8217;s a question about a relationship. No single chunk holds the answer because finding X is documented in one place and corrective action Y in another. A relationship connects them, not word overlap.</p><p>Vectors don&#8217;t traverse relationships. Graphs do.</p><p><strong>5. The intent problem.</strong></p><p>&#8220;What is two-factor authentication?&#8221; and &#8220;2FA broke after the update&#8221; look similar to a vector. They need completely different retrieval strategies. The first is exploration. The second is troubleshooting. Serving the same chunks for both means mediocre answers for both.</p><p>Intent classification before retrieval reshapes the search. Concept queries pull explanatory material and connected topics. Problem queries pull symptoms, root causes, and documented fixes. One knowledge base. Two very different paths through it.</p><h3>What a Graph Brings to the Table</h3><p>A knowledge graph stores entities and how they relate to each other. Not paragraphs. Structure.</p><p>Think of it this way: vector search says &#8220;here are documents that talk about similar things.&#8221; A graph says &#8220;here are the things connected to what you asked about, and here&#8217;s how they connect.&#8221;</p><p>The graph doesn&#8217;t replace the vector store. It sits next to it. Vectors find entry points. The graph fans out from there, pulling in related knowledge that no amount of cosine similarity would surface.</p><h4>The mechanics</h4><p>Three phases.</p><p><strong>Seed selection.</strong> Vector search returns top chunks by distance. A configurable subset becomes seeds for graph traversal. The remaining chunks still contribute to context, but only seeds enter the graph.</p><p><strong>Graph expansion.</strong> From each seed, traverse the graph a configurable number of hops. A chunk mentions entity A. Entity A is linked to entity B through RESOLVED_BY. Entity B has evidence in chunk C. Chunk C never appeared in vector results &#8212; it&#8217;s written in completely different language. But it contains the fix. Now it&#8217;s part of the context.</p><p><strong>Reranking.</strong> Blend vector scores with graph relationship scores. A chunk with a decent embedding match but strong graph connectivity to the query entities climbs above a chunk that&#8217;s merely close in vector space.</p><h3>Designing a Graph Schema for Your Domain</h3><p>No universal schema exists. What you model as nodes and what you model as edges determines everything the graph can reveal that vectors cannot. This is domain design, not framework configuration.</p><p>Two examples from domains I know.</p><h4>Customer support knowledge base</h4><p>The domain I&#8217;ve spent the most time building in.</p><p><strong>Entities.</strong> A single node type with a <code>kind</code> field distinguishing: Problem, Solution, Concept, Feature, Configuration. Problem is something that breaks. Solution is how to fix it. Concept is explanatory knowledge. Feature and Configuration model the product itself.</p><p><strong>Relationships.</strong> The schema lives or dies here.</p><p><code>CAUSES</code> &#8212; root cause to visible symptom. &#8220;Invalid webhook URL&#8221; CAUSES &#8220;Bot doesn&#8217;t respond.&#8221; User reports the symptom, graph traces back to the cause.</p><p><code>RESOLVED_BY</code> &#8212; problem to fix. &#8220;Bot doesn&#8217;t respond&#8221; RESOLVED_BY &#8220;Verify the webhook endpoint is publicly accessible.&#8221; Multiple solutions per problem. The model picks the most relevant given context.</p><p><code>DIAGNOSED_BY</code> &#8212; problem to diagnostic question. &#8220;Bot doesn&#8217;t respond&#8221; DIAGNOSED_BY &#8220;Does the webhook URL return 200 on a GET request?&#8221; This edge powers clarifying questions before the system commits to a full answer.</p><p><code>RELATED_TO</code> &#8212; bidirectional general association. &#8220;Webhook configuration&#8221; RELATED_TO &#8220;API key setup.&#8221; Supports conceptual browsing when the user is learning, not firefighting.</p><p><code>EVIDENCE_FOR</code> and <code>MENTIONS</code> &#8212; the bridge layer. They connect chunks (vector world) to entities (graph world). A documentation chunk is EVIDENCE_FOR a Solution, or MENTIONS a Feature. Without these edges, your two retrieval systems are blind to each other.</p><p><strong>Intent-aware traversal.</strong> Same graph, different paths depending on what the user needs. Problem intent traces CAUSES backward and RESOLVED_BY forward. HowTo intent follows RESOLVED_BY and looks for step-by-step content. Concept intent walks RELATED_TO broadly, mapping the neighborhood of connected knowledge.</p><p>Same nodes. Same edges. Different traversal logic.</p><h4>E-commerce product catalog</h4><p>Different world. Different schema. Identical principle.</p><p><strong>Entities.</strong> Product, Category, Feature, Specification, Review, Brand.</p><p><strong>Relationships.</strong></p><p><code>BELONGS_TO</code> &#8212; Product to Category. &#8220;Running Shoe X&#8221; BELONGS_TO &#8220;Men&#8217;s Athletic Footwear.&#8221;</p><p><code>HAS_FEATURE</code> &#8212; Product to Feature. &#8220;Running Shoe X&#8221; HAS_FEATURE &#8220;Carbon fiber plate.&#8221; Vector search catches the literal phrase. Graph traversal also finds every product sharing that feature node, including those describing it as &#8220;carbon-infused midsole&#8221; or &#8220;energy return plate.&#8221; Different vocabulary, same node.</p><p><code>COMPATIBLE_WITH</code> &#8212; Product to Product. &#8220;Running Shoe X&#8221; COMPATIBLE_WITH &#8220;Performance Insole Y.&#8221; This fact doesn&#8217;t live in any product description. It exists only as a relationship.</p><p><code>COMPARED_TO</code> &#8212; competitive links. &#8220;Which is better, X or Z?&#8221; &#8212; the graph pulls comparison context from both sides.</p><p><strong>The shared pattern:</strong> entities are things users ask about. Relationships are connections between those things that no individual text chunk captures. The graph holds the knowledge that lives between documents.</p><h3>When You Don&#8217;t Need a Graph</h3><p>A graph adds complexity to indexing, storage, and querying. It pays off when your data has entities with real relationships between them, when users ask relational questions, and when ambiguity shows up often.</p><p>If your use case is searching 500 FAQ articles and surfacing the closest match, vectors handle that fine. Don&#8217;t bolt on a graph because it looks sophisticated on an architecture diagram. Add one when users ask questions that require following connections between things.</p><h3>The Pipeline Is the Product</h3><p>RAG is not three steps. My pipeline has sixteen stages. Each exists because a specific production failure demanded it. Classification to skip needless retrieval. Intent detection to shape the search. Vector search for semantic matching. Graph expansion for relational knowledge. Reranking to merge signals. Summarization to fit attention windows. Ambiguity detection to stop confident wrong answers. Model selection to balance cost and quality.</p><p>Nothing was designed upfront. Every stage got added because something broke and needed to stop breaking.</p><p>The acronym promises simplicity. The engineering delivers something else entirely.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/p/why-rag-is-not-only-retrieval-augmentation?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/p/why-rag-is-not-only-retrieval-augmentation?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p>Thanks for reading.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/subscribe?"><span>Subscribe now</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Sanzo Wada's Colors in Your Code: How I Brought a 1930s Color Dictionary to AI and Web Development]]></title><description><![CDATA[A story about aesthetic, a forgotten Japanese artist, and turning 348 curated palettes into an MCP server that actually helps me build better interfaces.]]></description><link>https://labrodev.substack.com/p/sanzo-wadas-colors-in-your-code-how</link><guid isPermaLink="false">https://labrodev.substack.com/p/sanzo-wadas-colors-in-your-code-how</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sat, 04 Apr 2026 15:12:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!tdmw!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F34a63639-3b2a-4ca7-af73-5b23b9973ab5_400x400.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A story about aesthetic, a forgotten Japanese artist, and turning 348 curated palettes into an MCP server that actually helps me build better interfaces.</p><p>There is a particular kind of beauty that survives time.</p><p>Not because someone preserved it intentionally &#8212; but because it was so precisely right that people kept coming back. The proportions of the Parthenon. The color palette of a Vermeer interior. The typeface of a 1960s Swiss train schedule. These things carry a quiet authority. They don&#8217;t explain themselves. They just work.</p><p>Sanzo Wada&#8217;s color combinations are like that.</p><div><hr></div><h2><strong>Who was Sanzo Wada?</strong></h2><p>Sanzo Wada (1883&#8211;1967) was a Japanese artist, teacher, and costume designer. He spent decades studying color &#8212; not as an abstract concept, but as a practical tool. He worked in kimono design, textile production, and theater. He was interested in how colors affect perception, mood, and meaning in everyday objects and environments.</p><p>In the 1930s, he published <em>A Dictionary of Color Combinations</em> &#8212; a collection of 348 curated palettes built from 159 carefully selected colors. Each combination was assembled by hand, each swatch painted precisely. The book wasn&#8217;t a theory paper or an academic study. It was a reference guide: a craftsman&#8217;s tool for people who work with color and need reliable, harmonious combinations.</p><p>The original edition was published in Japanese, but the book eventually found a second life internationally. Seigensha Art Publishing reprinted it, and over the last few years, it quietly became a cult reference among designers, illustrators, and anyone who takes color seriously.</p><h2><strong>The Dictionary: Why It Matters</strong></h2><p>What makes Wada&#8217;s dictionary different from a random collection of palettes on Dribbble or Coolors?</p><p>Intent.</p><p>Every combination in the dictionary was composed with purpose. Wada wasn&#8217;t generating combinations algorithmically. He wasn&#8217;t running HSL calculations or color-wheel rules. He was painting swatches and looking at them &#8212; testing how they feel next to each other, how they behave in different contexts, how they create balance or tension.</p><p>The result is a collection that feels timeless. The palettes are not trendy. They don&#8217;t look like &#8220;2024 web design.&#8221; They don&#8217;t look like any particular era. They look &#8212; correct. In the same way that certain musical intervals sound right regardless of genre, Wada&#8217;s combinations carry a visual harmony that transcends fashion.</p><p>Each color in the dictionary includes not only its visual swatch but detailed technical data: hex, RGB, CMYK, and L*a*b* values. Each combination is either 2, 3, or 4 colors. Some are bold and contrasting. Some are quiet and tonal. Some feel like autumn. Some feel like the interior of a 1950s jazz club. All of them feel considered.</p><h2><strong>Color as a Material</strong></h2><p>In my article <em>Art in Everything: From Bruegel to Web Applications</em>, I wrote about creativity being everywhere &#8212; in facade renovations, in running shoe design, in writing code. Color is one of the most fundamental materials of that creativity. It is the first thing people perceive. Before they read your headline, before they understand your layout &#8212; they feel your colors.</p><p>And yet, color is one of the most neglected aspects of web development.</p><p>We spend hours optimizing database queries, structuring middleware, writing tests. Then we pick a color palette in five minutes &#8212; usually by copying some trending combination from a design tool or just going with Tailwind&#8217;s default slate and blue. It works. But it doesn&#8217;t sing.</p><p>Wada&#8217;s dictionary offers something different: a curated, time-tested library of combinations that were assembled by someone who spent a lifetime studying how colors interact. It&#8217;s like having a sommelier&#8217;s recommendations instead of grabbing a random bottle from the shelf. The random bottle might be fine. The recommendation will be better.</p><p>And the thing is &#8212; these palettes are not limited to art or print design. They apply to anything where color matters:</p><ul><li><p><strong>Web interfaces</strong> &#8212; landing pages, dashboards, SaaS products</p></li><li><p><strong>Brand identity</strong> &#8212; logos, marketing materials, social media presence</p></li><li><p><strong>Data visualization</strong> &#8212; charts, graphs, maps</p></li><li><p><strong>Presentations</strong> &#8212; slides that don&#8217;t look like default PowerPoint</p></li><li><p><strong>Design systems</strong> &#8212; building a consistent Tailwind or CSS custom theme</p></li></ul><p>Any surface that carries color can benefit from Wada&#8217;s eye.</p><h2><strong>From a Book to the Browser</strong></h2><p>A few years ago, Matt DesLauriers (<strong><a href="https://github.com/mattdesl">@mattdesl</a></strong>) digitized the color data from Wada&#8217;s dictionary and published it as a JSON dataset on GitHub under an MIT license. That dataset is a beautiful piece of work in itself &#8212; 159 colors with full hex/RGB/CMYK/L*a*b* values, and 348 combinations referencing those colors by ID.</p><p>When I found this dataset, the idea clicked almost immediately.</p><p>I wanted to build something around it. Not a static gallery. Not just another color-swatch website. I wanted to make Wada&#8217;s palettes actually usable &#8212; browsable, searchable, and most importantly, accessible programmatically. If I&#8217;m building a website and I want a good three-color palette, I should be able to ask for one and get it back as Tailwind config or CSS variables. If an AI assistant is helping me with a UI, it should be able to pull from Wada&#8217;s dictionary instead of making up random colors.</p><p>That&#8217;s how <strong><a href="https://espectro.dev/">espectro.dev</a></strong> was born.</p><p>Espectro is a free, non-commercial web project built with Laravel, Livewire, and Tailwind CSS. It serves the full Wada dictionary as:</p><ul><li><p><strong>A browsable site</strong> &#8212; explore all 159 colors and 348 combinations with swatches, detail pages, and search</p></li><li><p><strong>A public JSON API</strong> &#8212; no authentication required, rate-limited to 15 requests per minute, returning full color data in structured JSON</p></li><li><p><strong>An MCP server</strong> &#8212; so AI tools like Cursor and Claude can search colors, fetch palettes, and export combinations directly</p></li></ul><p>The site itself is intentionally minimal. Muted tones. Serif headings. Lots of white space. I wanted it to feel like the book &#8212; quiet, respectful, focused on the colors themselves.</p><h2><strong>The Practical Part: Color Palettes for Your Tailwind Config</strong></h2><p>Here&#8217;s where it gets useful for everyday development.</p><p>Say you&#8217;re building a dashboard. You need a primary color, a secondary, and an accent. Instead of guessing or scrolling through color pickers, you can hit the API:</p><pre><code><code>curl https://espectro.dev/api/combinations/42
</code></code></pre><p>You get back a structured JSON response with 2, 3, or 4 colors &#8212; each with hex, RGB, CMYK, and L*a*b*. Or if you want something random for inspiration:</p><pre><code><code>curl https://espectro.dev/api/combinations/random
</code></code></pre><p>But the real power comes from the export endpoint (available via MCP). Ask for combination #42 as a Tailwind config and you get:</p><pre><code><code>// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        'theme': {
          'primary': '#ebd999',
          'secondary': '#8b6c42',
          'accent': '#4a3728',
        }
      }
    }
  }
}
</code></code></pre><p>Or as CSS variables:</p><pre><code><code>:root {
  --color-primary: #ebd999;
  --color-secondary: #8b6c42;
  --color-accent: #4a3728;
}
</code></code></pre><p>Copy, paste, done. Your site now uses a palette that a Japanese color master composed 90 years ago. And it looks better than whatever you would have picked at 11 PM on a Friday.</p><p>This works for anything: landing pages, email templates, logo concepts, data dashboards, presentation themes. Wherever you need a cohesive set of colors that actually harmonize.</p><h2><strong>Making It an MCP Server</strong></h2><p>Here&#8217;s the part that excited me the most.</p><p>MCP &#8212; Model Context Protocol &#8212; is a standard that allows AI assistants to use external tools. If you&#8217;ve worked with Cursor or Claude, you&#8217;ve probably seen how they can call functions, search documentation, or interact with databases. MCP makes it possible to expose any service as a set of tools that an AI can invoke.</p><p>I built Espectro&#8217;s MCP server directly into the Laravel application. It exposes six tools:</p><p><strong>ToolWhat it does</strong><code>search-colors</code>Search colors by name or hex value<code>get-color</code>Get full details for a color by slug<code>search-combinations</code>Filter combinations by palette size (2, 3, or 4)<code>get-combination</code>Get a combination by ID with all color data<code>random-combination</code>Get a random palette<code>export-combination</code>Export as Tailwind config or CSS variables</p><p>The MCP endpoint is public at <code>https://espectro.dev/mcp/espectro</code> &#8212; the same rate limit applies (15 req/min per IP). Anyone can connect to it from any MCP-compatible client without installing anything.</p><p>For Laravel developers specifically, I also published a small connector package &#8212; <code>labrodev/laravel-mcp-espectro</code>. You run <code>composer require</code> and <code>php artisan espectro:install</code>, and it creates a <code>.mcp.json</code> in your project root. Cursor picks it up automatically. That&#8217;s it &#8212; no local MCP server, no configuration, no color data to bundle. The package simply points your tools to the hosted endpoint.</p><h2><strong>How I Actually Use It</strong></h2><p>I added Espectro as a connector in Claude Desktop. It took about 30 seconds: open settings, add a custom MCP server, paste the URL <code>https://espectro.dev/mcp/espectro</code>, save.</p><p>Now, when I&#8217;m working on a project and I need a color scheme, I don&#8217;t leave the conversation. I just say something like:</p><blockquote><p><em><strong>&#8220;I need a warm three-color palette for a bakery website. Check Sanzo Wada combinations and suggest something.&#8221;</strong></em></p></blockquote><p>Claude calls <code>search-combinations</code> with size 3, browses the results, and suggests specific combinations from Wada&#8217;s dictionary &#8212; with hex codes, visual descriptions, and even an exported Tailwind config if I ask for it. If I don&#8217;t like the first suggestion, I can say &#8220;something cooler, more muted&#8221; and it searches again, this time filtering by different criteria.</p><p>It&#8217;s not generating colors out of thin air. It&#8217;s pulling from a curated, historically validated source. And that&#8217;s the difference &#8212; instead of AI hallucinating a palette, it&#8217;s selecting from one of the most thoughtful color collections ever assembled.</p><p>I also use it in Cursor when I&#8217;m coding. If I&#8217;m styling a component and I need an accent color that works with my existing palette, I can ask Cursor to check Wada&#8217;s dictionary. It calls <code>search-colors</code>, finds matching options, and I pick one without leaving my editor.</p><p>This is what I meant in my article about AI and experience &#8212; AI amplifies your architecture. If you feed it good sources, you get good results. Wada&#8217;s dictionary is an exceptionally good source.</p><h2><strong>A Note on Public Domain and Credits</strong></h2><p>Sanzo Wada passed away in 1967. Under Japanese copyright law, his works entered the public domain 50 years after his death &#8212; in 2017. The color data used in Espectro comes from Matt DesLauriers&#8217; open-source digitization (MIT license) of the original published work.</p><p>Espectro is a free, non-commercial project. It does not assert any rights over the color data. No cookies, no tracking, no personal data collection on the API or MCP. It exists to make Wada&#8217;s work accessible and useful in the modern web development context.</p><p>If you enjoy working with these colors, I&#8217;d encourage you to buy the physical book &#8212; it&#8217;s available on Amazon and it&#8217;s a beautiful object to have on your desk. There&#8217;s something irreplaceable about seeing those swatches printed on paper, exactly as Wada intended.</p><div><hr></div><h2><strong>Final Thought</strong></h2><p>There is a particular kind of beauty that survives time.</p><p>Sanzo Wada spent decades learning what that beauty looks like in color. He encoded it into 348 combinations. Now, almost a century later, those combinations can flow from a book through a JSON file through an MCP server into your Tailwind config &#8212; and from there, onto someone&#8217;s screen.</p><p>The river is the same. The waters are ever newer.</p><div><hr></div><p><em>Espectro is open and free at <strong><a href="https://espectro.dev/">espectro.dev</a></strong>. Color data credits: <strong><a href="https://github.com/mattdesl/dictionary-of-colour-combinations">mattdesl/dictionary-of-colour-combinations</a></strong> (MIT). Developed by Petro Lashyn.</em></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[What matters the most for me after 15 years of experience — reflections triggered by AI integration]]></title><description><![CDATA[On simplicity, boundaries, and building software that lasts]]></description><link>https://labrodev.substack.com/p/what-matters-the-most-for-me-after</link><guid isPermaLink="false">https://labrodev.substack.com/p/what-matters-the-most-for-me-after</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Thu, 29 Jan 2026 13:27:57 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!HxkY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>A short reflection on structure, simplicity, and experience</h3><p>Recently, I faced a task that made me pause and reflect on one of my larger projects.</p><p>But very quickly, it became clear that I wasn&#8217;t just reflecting on the project itself &#8212; I was reflecting on my approach to building software in general.</p><p>Over the years, I&#8217;ve worked in many roles: PHP developer, Laravel developer, software architect, DevOps, frontend developer, full-stack developer. Sometimes all of them at once. Sometimes by choice, sometimes because the project demanded it.</p><p>When I didn&#8217;t know something, I figured it out. At first with Stack Overflow, experiments, patterns, and trial and error. Later with frameworks, best practices, and philosophies. SOLID, DRY, KISS. Object-oriented programming, abstractions, factories, contracts, interfaces. Mixing Laravel with ideas from Symfony. Falling back to simple MVC when it made sense.</p><p>I&#8217;ve worked on single-page applications across different generations: early Angular, then Vue, then Vue 3 with a completely different paradigm. React, TypeScript, Tailwind, Inertia. Domain-driven design. Layered architectures. Large projects with many models and tables, APIs, dashboards for different actors, background jobs, console scripts, and integrations &#8212; all sharing the same core.</p><p>I didn&#8217;t build unicorns.</p><p>I didn&#8217;t work in Big Tech.</p><p>But for more than 15 years, I&#8217;ve worked on real projects &#8212; SaaS, CRM, ERP, small pet projects, experimental ones &#8212; and many of them are still running today.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!HxkY!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!HxkY!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 424w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 848w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!HxkY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg" width="1456" height="892" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:892,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3047968,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/186190368?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!HxkY!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 424w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 848w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!HxkY!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F927065c0-a0b9-4bee-b181-c27b6203a4c2_4361x2672.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3>Why I started reflecting more consciously now</h3><p>The real trigger for this reflection was AI.</p><p>I&#8217;ve recently started using AI as a serious resource in my development process &#8212; not as a toy, but as a tool to speed up work, explore solutions, refactor code, and challenge my own decisions.</p><p>AI has an interesting side effect: it forces you to be explicit.</p><p>If your architecture is blurry, AI amplifies the blur.</p><p>If responsibilities are mixed, AI happily generates more mixed logic.</p><p>If boundaries are unclear, everything starts leaking everywhere.</p><p>That&#8217;s when I realized: this is a good moment to consciously draw boundaries, simplify decisions, and reflect on what actually matters in my daily work as a developer.</p><p>After compressing all that experience, I ended up with just three simple statements.</p><div><hr></div><h4>1. Keep it simple, Smart</h4><p>This sounds obvious, but it&#8217;s still the most important thing.</p><p>KISS really works.</p><p>Not &#8220;simple&#8221; in a dumb way &#8212; but simple in a smart, intentional way. So literally, <strong>Keep It Simple, <s>Stupid</s> Smart.</strong></p><p>When you come back to a piece of code after a year &#8212; to add a feature, debug a strange 500 error, or adapt to new business logic &#8212; simplicity becomes priceless. If the code is straightforward, you immediately understand what&#8217;s going on. You don&#8217;t need to re-learn layers of abstractions or trace hidden flows.</p><p>Simple code is easier to maintain.</p><p>Simple code is easier to debug.</p><p>Simple code is easier for others to work with.</p><p>And future you is also &#8220;someone else&#8221;.</p><h4>2. Separate concerns and make the structure ex<strong>plicit</strong></h4><p>Simplicity does not mean chaos.</p><p>It&#8217;s extremely important to separate concerns and build a clear structure &#8212; whether it&#8217;s a Laravel project, a package, or a Vue SPA.</p><p>How you do this is up to you: frameworks, patterns, naming, folder structure. That part is personal. But once you choose a structure, it must be logical and predictable.</p><p>You should clearly understand:</p><ul><li><p>where responsibilities live,</p></li><li><p>how data flows through the system,</p></li><li><p>how actors move through the application.</p></li></ul><p>The system should be decomposed into layers that can live independently: application layer, domain or service layer, view or view-model layer. These layers should communicate through well-defined boundaries and be replaceable.</p><p>You should always have a clear picture in your head of how the flow moves through these layers and what exactly happens in each of them.</p><h4>3. Have a single source of truth</h4><p>This principle connects everything above.</p><p>A single source of truth works beautifully &#8212; and it&#8217;s closely related to both separation of concerns and simplicity.</p><p>For example, when you perform mutating operations on a model &#8212; creating records, updating fields, deleting data, changing state &#8212; there should be one clearly defined place where this logic lives.</p><p>Not spread across controllers, services, jobs, and helpers.</p><p>Not duplicated in slightly different forms.</p><p>The same applies to reading data, orchestration, business rules, and configuration. Each concern should have its own source of truth.</p><p>When you work this way:</p><ul><li><p>behavior becomes predictable,</p></li><li><p>systems become easier to reason about,</p></li><li><p>changes become safer.</p></li></ul><p>You always know where to look.</p><p>You always know what to change.</p><div><hr></div><h3>Final note</h3><p>None of these thoughts are new or revolutionary.</p><p>But they are the things that, based on my experience, really matter for my work today. They help me stay productive, calm, and confident when working on real projects &#8212; especially now, when AI is part of the development process.</p><p>This is simply what works for me.</p><p>Glad to hear your reflections! And thanks for an attention!</p><div><hr></div><p>Image credit:<a href="https://unsplash.com/@jontyson"> https://unsplash.com/@jontyson</a></p>]]></content:encoded></item><item><title><![CDATA[TRIZ: an engineering way to think about problem solving]]></title><description><![CDATA[Resolving contradictions instead of balancing them]]></description><link>https://labrodev.substack.com/p/triz-an-engineering-way-to-think</link><guid isPermaLink="false">https://labrodev.substack.com/p/triz-an-engineering-way-to-think</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Wed, 17 Dec 2025 12:40:58 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EuWk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Problem solving and why the approach matters</h3><p>In engineering, we solve problems all the time. Some problems are straightforward. Something is broken, we fix it. Something is slow, we optimize it. But many problems are not like that.</p><p>We often face situations where:</p><ul><li><p>improving one thing makes another thing worse</p></li><li><p>every option looks reasonable, but none feels right</p></li><li><p>any decision comes with a clear downside</p></li></ul><p>Examples are familiar:</p><ul><li><p>make the system more flexible, and it becomes harder to understand</p></li><li><p>add safety checks, and performance drops</p></li><li><p>simplify logic, and future changes become harder</p></li></ul><p>These are not bugs.</p><p>These are <strong>engineering dilemmas</strong>.</p><p>At this point, the solution depends not only on <em>what</em> we do, but on <em>how</em> we think about the problem. Different approaches lead to very different results.</p><p>One of those approaches is TRIZ.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EuWk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EuWk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 424w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 848w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EuWk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1045560,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/181883067?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EuWk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 424w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 848w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!EuWk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0cd4a6db-3310-4fc2-986d-dba9f7524867_7952x5304.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><div><hr></div><h3>What is TRIZ (and what it is not)</h3><p>TRIZ stands for <strong>Theory of Inventive Problem Solving</strong>.</p><p>It was developed by studying a large number of engineering inventions and patents and looking for patterns in how difficult problems were solved.</p><p>Despite the name, TRIZ is not abstract theory.</p><p>It is a practical, engineering-focused way to analyze problems.</p><p>A common misunderstanding:</p><p>TRIZ is often associated with the Russian language because the original materials were written in Russian. But TRIZ itself is not &#8220;about Russian engineering&#8221; or culture. It is a universal approach, based on patterns found across many industries and countries.</p><p>TRIZ is not:</p><ul><li><p>brainstorming</p></li><li><p>intuition-based creativity</p></li><li><p>a list of clever tricks</p></li></ul><p>TRIZ is about structured thinking.</p><p>In simple terms, TRIZ says:</p><blockquote><p>Many hard problems are hard because of contradictions inside the system.</p><p>If we understand those contradictions, we can find better solutions.</p></blockquote><div><hr></div><h3>The basic TRIZ cycle: problem &#8594; dilemma &#8594; contradiction &#8594; solution</h3><p>TRIZ often works as a cycle.</p><p><strong>Problem</strong></p><p>Something needs to be improved.</p><p><strong>Dilemma</strong></p><p>Improving one thing makes another thing worse.</p><p><strong>Contradiction</strong></p><p>We clearly describe what exactly conflicts with what.</p><p><strong>Solution</strong></p><p>We change the system so the conflict no longer exists in the same way.</p><p>The key step here is moving from <em>dilemma</em> to <em>contradiction</em>.</p><p>A dilemma is vague: &#8220;this or that&#8221;.</p><p>A contradiction is precise: &#8220;when I improve X, Y becomes worse&#8221;.</p><p>Once the contradiction is clear, the solution space becomes wider.</p><div><hr></div><h3>Real-world examples using the TRIZ cycle</h3><h4>Example 1: Fast but safe transportation</h4><p><strong>Problem:</strong> move people faster</p><p><strong>Dilemma:</strong> higher speed increases danger</p><p><strong>Contradiction:</strong> speed improves efficiency but reduces safety</p><p>A compromise would be &#8220;not too fast&#8221;.</p><p>A TRIZ-style solution changes the system:</p><ul><li><p>separate traffic flows</p></li><li><p>add safety systems that activate only in dangerous situations</p></li><li><p>automate parts of control</p></li></ul><p>High-speed trains are not a compromise.</p><p>They are a resolution of the contradiction.</p><h4>Example 2: Simple devices with many functions</h4><p><strong>Problem:</strong> one device should do many things</p><p><strong>Dilemma:</strong> more functions mean more complexity</p><p><strong>Contradiction:</strong> functionality increases value but reduces simplicity</p><p>Instead of adding more buttons:</p><ul><li><p>functions are hidden behind modes</p></li><li><p>software replaces hardware controls</p></li><li><p>interfaces change depending on context</p></li></ul><p>The device stays simple <em>at the moment of use</em>.</p><h4>Example 3: Software development</h4><p><strong>Problem:</strong> ship features quickly</p><p><strong>Dilemma:</strong> fast changes reduce stability</p><p><strong>Contradiction:</strong> speed improves delivery but increases risk</p><p>TRIZ-style solutions include:</p><ul><li><p>automated testing</p></li><li><p>feature flags</p></li><li><p>staging environments</p></li></ul><p>Speed and stability are no longer in direct conflict.</p><p>The system is changed so they coexist.</p><div><hr></div><h3>TRIZ in real companies and practice</h3><p>TRIZ is not a niche idea.</p><p>Elements of TRIZ thinking have been used by:</p><ul><li><p>Samsung (systematic innovation programs)</p></li><li><p>Intel (engineering problem analysis)</p></li><li><p>Toyota (process improvement and contradictions in manufacturing)</p></li></ul><p>Many modern engineering practices reflect TRIZ ideas, even if they are not labeled as such.</p><p>People often use TRIZ principles without knowing the name.</p><div><hr></div><h3>Conclusion: applying TRIZ beyond engineering</h3><p>At its core, TRIZ is about <strong>how we frame problems</strong>.</p><p>Instead of asking:</p><ul><li><p>&#8220;Which option is better?&#8221;</p></li></ul><p>We ask:</p><ul><li><p>&#8220;Why do these things conflict?&#8221;</p></li><li><p>&#8220;Can the system be changed so they don&#8217;t?&#8221;</p></li></ul><p>This way of thinking works not only in engineering.</p><p>It applies to:</p><ul><li><p>work processes</p></li><li><p>business decisions</p></li><li><p>team organization</p></li></ul><p>Anywhere trade-offs appear, contradictions exist.</p><p>TRIZ does not promise perfect answers.</p><p>It offers something more valuable:</p><p>a disciplined way to think before accepting limits.</p><p>And often, that is enough to find a better path.</p><div><hr></div><p>Thanks for your attention! </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/subscribe?"><span>Subscribe now</span></a></p><p>Image credit: https://unsplash.com/@thisisengineering </p>]]></content:encoded></item><item><title><![CDATA[Let's Encrypt on Ubuntu + Nginx Tutorial]]></title><description><![CDATA[Install, configure, and auto&#8209;renew free SSL certificates to save costs on SSL infrastructure]]></description><link>https://labrodev.substack.com/p/lets-encrypt-on-ubuntu-nginx-tutorial</link><guid isPermaLink="false">https://labrodev.substack.com/p/lets-encrypt-on-ubuntu-nginx-tutorial</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Wed, 20 Aug 2025 09:34:24 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3b1dcc48-a769-46f1-b905-4d5c48f7a9ff_4032x3024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently totally turned from commercially issued SSL certificates for different projects and switched completely to <strong><a href="https://letsencrypt.org/">Let&#8217;s Encrypt</a></strong> way of handling SSL certificates for domains. </p><p>In this article I would like to share playbook how to install, configure and set up a process for auto-renewal (it&#8217;s actual because normal Let&#8217;s Encrypt certificate lifetime is 90 days). </p><p>But we jump deep in practical manual parts, let&#8217;s just do a quick refresher what is SSL and what is Let&#8217;s Encrypt. </p><h3>What is SSL an why it&#8217;s important?</h3><p>When people say <em>SSL</em>, they usually mean <em>TLS</em> (Transport Layer Security). It&#8217;s the technology that makes the connection between your browser and a server <strong>encrypted and verified</strong>. In practice, that means:</p><ul><li><p>The server proves it&#8217;s really the site you wanted to reach (using a certificate and private key).</p></li><li><p>The browser checks that certificate against a trusted authority (Certificate Authority).</p></li><li><p>Once trust is established, all traffic is scrambled with encryption so nobody in between can read or change it.</p></li></ul><p>So passwords, forms, payments, API calls &#8212; everything stays safe from eavesdropping or tampering. In 2025, you can&#8217;t run a serious site without it: browsers warn users, Google ranks HTTPS sites higher, and security compliance flat-out requires it.</p><h3>What is Let&#8217;s Encrypt and why it&#8217;s free</h3><p>Let&#8217;s Encrypt is a <strong>Certificate Authority (CA)</strong> &#8212; the trusted third party that hands out SSL/TLS certificates. Normally, companies like RapidSSL or DigiCert sell you a certificate for money every year. Let&#8217;s Encrypt flipped the model: it&#8217;s a <strong>non-profit, backed by Mozilla, EFF, and others</strong>, that gives certificates away for free.</p><p>Why? Because encrypted web traffic should be the default, not a luxury. Their funding comes from sponsors and donations, not from charging website owners.</p><p>And the really cool part here is automation: instead of filling out forms, emailing back and forth, and paying invoices, you run a simple tool (certbot), and within seconds your server gets a valid certificate. It also renews automatically, so you don&#8217;t wake up one morning with a broken &#8220;expired cert&#8221; warning.</p><p>Personally, dealing with SSL certificates was always a headache. You had to contact the client, explain what you needed, ask for credentials to log in to the issuer&#8217;s site and fill out forms. Or sometimes you&#8217;d just wait for the client to send you the freshly issued certificate &#8212; often in some unexpected format, and almost always with a file or two missing. Sounds familiar, doesn&#8217;t it? </p><p>So Let&#8217;s Encrypt not only save costs for you and for your clients, but also make the process of handling the certificates drastically simpler. And in the guide below, you&#8217;ll see how effortless it really is. </p><div><hr></div><h3>Let&#8217;s Encrypt installation and configuration guide </h3><p>Below is the step-by-step instruction how to install and configure Let&#8217;s Encrypt certificates on your Ubuntu + Nginx server (probably the most common combination of server setup). </p><p>In this example let&#8217;s charge a certificate for domain labrodev.com.</p><h4>Install Certbot and the Nginx plugin</h4><pre><code>sudo apt update
sudo apt install -y certbot python3-certbot-nginx</code></pre><h4>Make sure Nginx has a matching server block </h4><p>Certbot&#8217;s Nginx plugin needs to <strong>find</strong> your vhost by server_name. Minimal HTTP block:</p><pre><code># /etc/nginx/sites-available/labrodev.com
server {
    listen 80;
    listen [::]:80;
    server_name labrodev.com www.labrodev.com;

    root /var/www/labrodev;  # your path
    index index.html index.htm index.php;
}</code></pre><p>Before issuing a new certificate for your domain, make sure you have an Nginx config in sites-available (and symlinked in sites-enabled) with a server_name that exactly matches the domain you&#8217;re requesting the certificate for.</p><p>If it&#8217;s not enabled yet (no symlink in sites-enabled), enable it before moving for next step:</p><pre><code>sudo ln -s /etc/nginx/sites-available/labrodev.com /etc/nginx/sites-enabled/
sudo nginx -t &amp;&amp; sudo systemctl reload nginx</code></pre><h4>Issue certificate </h4><pre><code>sudo certbot --nginx -d labrodev.com -d www.labrodev.com</code></pre><p>What happens here:</p><ul><li><p>Certbot serves an HTTP&#8209;01 challenge via Nginx.</p></li><li><p>On success, it writes certs under /etc/letsencrypt/live/labrodev.com/.</p></li><li><p>It <strong>modifies your Nginx server block</strong> to add ssl_certificate and ssl_certificate_key, and (if you accept) an HTTP&#8594;HTTPS redirect.</p></li></ul><p>So after the certificate is issued, your vhost for this domain with such server name will have automatically added record for <strong>ssl_certificate</strong> and <strong>ssl_certificate_key</strong>:</p><pre><code>server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name labrodev.com www.labrodev.com;

    ssl_certificate /etc/letsencrypt/live/labrodev.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/labrodev.com/privkey.pem;

    root /var/www/labrodev;
    index index.html index.htm index.php;
}</code></pre><h4>Configure auto-renewal process</h4><p><strong>Let&#8217;s verify the systemd timer exists. </strong></p><p>Certbot (APT) installs a timer that runs renew checks twice daily:</p><pre><code>systemctl list-timers | grep certbot
systemctl status certbot.timer</code></pre><p><strong>Dry&#8209;run renewal </strong></p><p>This command will simulate renewal (with &#8212;dry-run option) of certificates:</p><pre><code>sudo certbot renew --dry-run</code></pre><p>You should see something like &#8220;Congratulations, all simulated renewals succeeded!&#8221; message.</p><h4>Reload Nginx automatically after real renewals </h4><p>Create a post&#8209;renew hook that syntax&#8209;checks and reloads Nginx only when certs actually renew:</p><pre><code>sudo tee /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh &gt;/dev/null &lt;&lt;'EOF'
#!/bin/bash
# Only runs after a successful *real* renewal event.
# Validate config before reloading to avoid downtime.
if /usr/sbin/nginx -t; then
  systemctl reload nginx
fi
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh</code></pre><h4>Simulate the end-to-end flow</h4><p>I like to confirm the entire chain (renew &#8594; nginx test &#8594; reload) in one go:</p><pre><code>sudo certbot renew --dry-run &amp;&amp; sudo nginx -t &amp;&amp; sudo systemctl reload nginx</code></pre><p>Dry&#8209;run won&#8217;t actually change certs, but you&#8217;ll see the path is healthy and your reload command works.</p><h4>Double check the setup </h4><p>There are some proof commands that feel you secure about whole setup and which you actually can use from time to time to check the state of certificates. Below there are some of these useful commands.</p><p><strong>When does auto&#8209;renew run next / when did it last run?</strong></p><pre><code>systemctl list-timers | grep certbot
systemctl status certbot.timer
journalctl -u certbot --since "3 days ago"</code></pre><p><strong>What version am I running / which binary?</strong></p><pre><code>certbot --version
which certbot</code></pre><p><strong>List all installed certificates and their expiry </strong></p><pre><code>sudo certbot certificates</code></pre><p><strong>Check the expiry of a specific PEM</strong></p><pre><code>sudo openssl x509 -enddate -noout -in /etc/letsencrypt/live/labrodev.com/fullchain.pem
# notAfter=Nov 17 11:05:44 2025 GMT</code></pre><div><hr></div><h3>What about wildcard certificates?</h3><p>A wildcard certificate is one that covers <strong>all subdomains under a given domain</strong>. For example, a cert for *.labrodev.com would automatically secure dev.labrodev.com, test.labrodev.com, api.labrodev.com, and so on &#8212; without you needing to issue separate certs for each one. This is handy if your product spins up subdomains dynamically (e.g. customer1.yourapp.com, customer2.yourapp.com), since managing dozens or hundreds of individual certificates would be a nightmare.</p><p>Let&#8217;s Encrypt can issue wildcard certificates, but there&#8217;s a catch: you need to prove ownership via <strong>DNS-01 validation</strong> (adding TXT records in your DNS). That adds complexity, because it means touching DNS settings rather than just letting Certbot hook into Nginx.</p><p>Personally, my approach is <strong>simpler</strong>: I just treat each subdomain I use as a separate domain with its own Nginx vhost and Let&#8217;s Encrypt certificate. So labrodev.com, dev.labrodev.com, and test.labrodev.com each have their own config and their own cert. It works perfectly fine if you only have a handful of subdomains.</p><p>Of course, if your platform generates subdomains on the fly dynamically, that&#8217;s when a wildcard makes sense &#8212; but that&#8217;s a bigger topic for another day and not for this article.</p><div><hr></div><h3>Conclusion</h3><p>So nearly in ~10 minutes you can:</p><ul><li><p>Install Let&#8217;s Encrypt on Ubuntu,</p></li><li><p>Issue trusted certs via the Nginx plugin,</p></li><li><p>Enable <strong>hands&#8209;off auto&#8209;renewal</strong>,</p></li><li><p>And wire a <strong>post&#8209;renew hook</strong> so Nginx picks up fresh certs automatically.</p></li></ul><p>Short&#8209;lived certs + automated renewals are exactly how SSL should work in 2025: secure by default, zero manual babysitting, and no licensing friction. It&#8217;s simple, robust, and free.</p><p>I hope you enjoy this article! </p><p>Have a luck with this and keep your domains secure! </p><p>If you like it or find useful, you may share it with your audience. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/p/lets-encrypt-on-ubuntu-nginx-tutorial?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/p/lets-encrypt-on-ubuntu-nginx-tutorial?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><div><hr></div><p>If you like Labro:Dev blog and topics we cover here, we will glad to see your in our subscribers list. Let&#8217;s connect!</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>Preview image credit:<a href="https://unsplash.com/@franckinjapan"> https://unsplash.com/@franckinjapan</a></p>]]></content:encoded></item><item><title><![CDATA[Building a Simple PHP Composer Package]]></title><description><![CDATA[Make it clean, make it shareable: writing your own Composer package with good organized structure.]]></description><link>https://labrodev.substack.com/p/building-a-simple-php-composer-package</link><guid isPermaLink="false">https://labrodev.substack.com/p/building-a-simple-php-composer-package</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sun, 06 Jul 2025 11:27:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a9ef5231-3a26-44bc-a4d6-651fc718ea41_6654x4436.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Introduction</h3><h5>Why we need composer packages? </h5><p><a href="https://getcomposer.org/">Composer</a> is the de facto package manager for PHP, making it easy to share and reuse code across projects. By <strong>creating a reusable Composer package</strong>, you can modularize your code and avoid &#8220;reinventing the wheel&#8221; in future projects. It&#8217;s also satisfying and fun to open-source your work &#8211; not only do <em>you</em> benefit from reusability, but other developers can discover and use your package, potentially improving it with feedback or contributions. In the vast PHP ecosystem, thousands of packages on Packagist (the main Composer repository) exemplify how sharing solutions benefits the community. In short, building a package encourages better code organization, and publishing it publicly gives others the opportunity to learn from or build upon your work.</p><h5>What inside this article? </h5><p>In this article, we&#8217;ll walk through how to build a simple PHP package and publish it on Packagist, using a real example: a <strong><a href="https://github.com/labrodev/postal-formatter">&#8220;Postal Formatter&#8221;</a></strong> library that we&#8217;ve built to use in different projects. This package provides a handy utility to format European postal codes according to each country&#8217;s official standards. We&#8217;ll cover the journey of creating the package, setting it up for Composer, pushing it to GitHub, releasing it via Packagist, and even highlight a neat PHP 8.1 feature (Enums) used in the package. By the end, you should have a clear roadmap for creating and sharing your own PHP library.</p><div><hr></div><h3>Bring it to the world!</h3><p>So let&#8217;s start with the publishing process.  </p><p>After writing your PHP library, how do you make it installable via Composer for anyone in the world? The answer is <a href="https://packagist.org/">Packagist.org</a> &#8211; the central repository for Composer packages.</p><p>The process is quite straightforward described below and can&#8217;t be represented as to-do guide with some steps: </p><ol><li><p><strong>Prepare a GitHub Repository:</strong> Create a new public repository on GitHub for your package code. At minimum, your repo should contain a composer.json file at its root with the package&#8217;s metadata (name, description, author, PHP version requirement, etc.). This file is essential &#8211; Packagist will read it to get your package info . For example, your composer.json should declare a unique package name in the format "vendor/package" (e.g., "labrodev/postal-formatter") and the minimum PHP version, among other details. You should also include a README with usage instructions and a License file (more on these later).</p></li><li><p><strong>Implement Your Package Code:</strong> Develop your library code under the repository. Follow PSR-4 autoloading by organizing classes into directories matching your namespace (configured in composer.json). For instance, if your package&#8217;s namespace is Labrodev\PostalFormatter, the source files might live under a src/ directory. Ensure your code is committed and pushed to the GitHub repo.</p></li><li><p><strong>Add a Version Tag:</strong> Before submitting to Packagist, tag a release version in your Git repository. This step assigns a version number to your code. Using <a href="https://semver.org/">semantic versioning</a> is recommended (e.g., v1.0.0 for your first release). You can create a tag via command line:</p><pre><code>git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0</code></pre><p>Packagist automatically detects versions from Git tags. In fact, new versions of your package are fetched from the tags you create in your VCS repository, so it&#8217;s best to omit a fixed version in composer.json and just rely on Git tags like 1.0.0 or v1.0.0 . This way, each Git tag (v1.0.1, v1.1.0, etc.) will appear as a version that users can require via Composer.</p></li><li><p><strong>Submit to Packagist:</strong> Create an account on <a href="https://packagist.org">Packagist.org</a> (you can sign in with GitHub for convenience). Once logged in, click the &#8220;Submit&#8221; button on Packagist&#8217;s top menu. In the submission form, provide the <strong>public Git repository URL</strong> of your package (for example, the GitHub URL). Packagist will import your repository &#8211; reading the composer.json to gather package details automatically . After submitting, your package will have its own Packagist page, and developers can install it via Composer.</p></li><li><p><strong>Enable Auto-Updates (Optional but Recommended):</strong> By default, Packagist will periodically check your repo for new tags. To get immediate updates on new releases, you can hook up Packagist to your GitHub. If you logged in via GitHub and authorized Packagist, it can set up a service hook so that every time you push a new tag, Packagist notices right away . This ensures your users can fetch the latest version as soon as you release it.</p></li></ol><p>Once these steps are done, your library is live on Packagist! For example, if your package is named vendor/package, anyone can run composer require vendor/package to pull it into their project . In our case, we used labrodev/postal-formatter as the package name, so developers can install it with a simple command:</p><pre><code>composer require labrodev/postal-formatter</code></pre><p>Next, let&#8217;s dive into the <strong>Postal-Formatter</strong> package itself as a case study of how a simple Composer package is structured and what it contains.</p><div><hr></div><h3>Case Study: Inside the Postal-Formatter Package</h3><p><strong>Postal-Formatter</strong> is a small PHP library that <strong>formats European postal codes</strong> in a consistent way. Imagine you have postal/zip codes from various countries &#8211; this tool will clean them up and output them in the official format for that country. For example, if given a UK code "sw1a1aa", the library will output "SW1A 1AA" with the proper capitalization and spacing; if given a Czech Republic code "12345", it will output "123 45" with the standard space in the middle . It supports over 45 country formats (essentially most of Europe) and normalizes input by removing any extraneous characters and uppercasing letters . In short, it&#8217;s a handy utility if you&#8217;re dealing with international postal addresses and want to ensure the codes are uniformly formatted.</p><h4>Features and Usage</h4><p>To summarize what our tiny package offers, here are its key features (as described in the README):</p><ul><li><p><strong>Cleans and standardizes</strong> postal code input (trims whitespace, removes non-alphanumeric characters, and uppercases letters) .</p></li><li><p><strong>Supports 45+ European country codes</strong>, each with country-specific formatting rules (ISO 3166-1 alpha-2 country codes are used for identifying countries) .</p></li><li><p><strong>Auto-formats codes per country standard</strong> &#8211; e.g., "12345" becomes "123 45" for Czech Republic (CZ), "LV1234" becomes "LV-1234" for Latvia, or "sw1a1aa" becomes "SW1A 1AA" for Great Britain (GB) .</p></li><li><p>Provides a <strong>simple static interface</strong> for formatting (a single PostalFormatter::format() method handles everything).</p></li><li><p>Written for <strong>strict typing in PHP 8.1+</strong> (uses declare(strict_types=1) and typed class properties/methods), ensuring type safety.</p></li><li><p>Includes a <strong>test suite (PHPUnit)</strong> and <strong>static analysis (PHPStan)</strong> configuration, which means the code is verified for correctness and adherence to best practices.</p></li></ul><p>Using the package is straightforward. We just call the static format method. For example:</p><pre><code>use Labrodev\PostalFormatter\Utilities\PostalFormatter;

echo PostalFormatter::format(' 12345 ');          
// Outputs: 12345  (default normalization)

echo PostalFormatter::format('12345', 'CZ'); 
// Outputs: 123 45 (Czech format)

echo PostalFormatter::format('sw1a1aa', 'GB'); 
// Outputs: SW1A 1AA (UK format)

echo PostalFormatter::format('1050', 'LV');  
// Outputs: LV-1050 (Latvia format)</code></pre><p>Under the hood, the formatter will throw an exception if you provide an unsupported country code (to avoid silently failing or mis-formatting). For instance, PostalFormatter::format('12345', 'XX') would throw an InvalidCountryCode exception, because &#8220;XX&#8221; is not a valid ISO country code in its list.</p><h4>Project Structure and Notable Files</h4><p>One goal of this case study is to illustrate how a simple Composer package is organized. The <strong>Postal-Formatter</strong> repository follows common best practices for PHP libraries:</p><ul><li><p><strong>composer.json:</strong> This file defines the package metadata. It includes the package name (labrodev/postal-formatter), a description, keywords, the minimum PHP version (8.1), autoload information (PSR-4 mapping of the namespace to the src/ directory), and any dependencies. It also lists development requirements like PHPUnit and PHPStan, and even defines convenient Composer scripts (e.g., "test" to run the test suite, "analyse" to run PHPStan) to streamline development. This file is crucial, as Packagist reads it to register your package and Composer uses it to resolve dependencies.</p></li><li><p><strong>README.md:</strong> A comprehensive README is provided, which serves as the documentation for the package. It typically explains the package&#8217;s purpose, features, installation instructions, usage examples, and any other important notes. In Postal-Formatter&#8217;s README, for example, you&#8217;ll find the list of features and code examples we discussed above, plus sections on running tests and static analysis. A good README is important for any open-source package &#8211; it&#8217;s the first thing developers will read on GitHub or Packagist to understand how to use your library.</p></li><li><p><strong>CHANGELOG.md (or CHANGES.md):</strong> It&#8217;s a good practice to include a changelog file listing the changes in each version of your package. This helps users see what&#8217;s new or fixed when you tag a new release. In our package, a CHANGES.md file outlines updates across versions (e.g., new country formats added, bug fixes, etc.). Maintaining this is especially useful as your package evolves.</p></li><li><p><strong>LICENSE:</strong> An open-source license file (in this case, MIT License) is included, allowing others to know the terms under which they can use and distribute the package. MIT is a permissive license, encouraging wide usage. Make sure to choose a license for your package &#8211; lack of a clear license can deter potential users or contributors.</p></li><li><p><strong>src/ Directory:</strong> This contains the PHP source code for the library, organized by namespace. For Postal-Formatter:</p><ul><li><p>Utilities/PostalFormatter.php &#8211; the main utility class with the format() method. It handles cleaning the input and delegating to country-specific formatting if a country code is provided.</p></li><li><p>Enums/CountryCode.php &#8211; an <strong>Enum</strong> defining supported country codes (like CZ, GB, FR, etc.) with each enum case encapsulating the logic to format a postal code for that country. We&#8217;ll discuss this Enum in detail in the next section.</p></li><li><p>Exceptions/InvalidCountryCode.php &#8211; a custom exception class thrown when an unknown country code is used. This is a simple class extending PHP&#8217;s Exception, providing a static make($code) method to create a standardized error message.</p></li></ul></li><li><p><strong>tests/ Directory:</strong> Contains unit tests (using PHPUnit) to verify the package&#8217;s behavior. For instance, Postal-Formatter has tests ensuring that PostalFormatter::format() returns expected outputs for a variety of inputs and country codes. There&#8217;s also a test to ensure an invalid country code triggers the appropriate exception. Automated tests give confidence that the package works as intended and that future changes won&#8217;t break existing functionality.</p></li></ul><p>By structuring the project in this way, we ensure that it&#8217;s easy to navigate and maintain. When others browse your repository or install the package, they can quickly find the documentation (README), see how to run tests, and understand how the code is organized. These are hallmarks of a well-crafted Composer package.</p><h4>Quality Assurance: Static Analysis and Testing</h4><p>As mentioned, our example package emphasizes code quality by including <strong>static analysis and testing tools</strong>:</p><ul><li><p><strong>PHPUnit Tests:</strong> Having a suite of tests is crucial for any package. In Postal-Formatter, the tests cover various country formats (e.g., formatting a Polish postal code should insert a dash after the first two digits, formatting a UK code should insert a space in the right spot, etc.) and edge cases (like handling already formatted input or throwing exceptions on bad input). To run the tests, one can simply execute composer test (thanks to the script in composer.json), which runs PHPUnit. All tests passing means our formatter works for all supported countries as expected.</p></li><li><p><strong>PHPStan Static Analysis:</strong> The package also includes a PHPStan configuration (phpstan.neon.dist) and a Composer script composer analyse to run static analysis. PHPStan goes through the code to catch potential bugs or type errors without even running the code. By running static analysis, we ensure that our code has no obvious mistakes, that we&#8217;re not calling undefined methods, passing wrong types, etc. Using PHPStan at level max (or a high level) enforces strict, bug-free code. The inclusion of "declare(strict_types=1)" at the top of PHP files further ensures that type declarations are honored, preventing unintended type juggling. Embracing these tools (tests and static analysis) means the package is more robust and trustworthy for users. It&#8217;s great to highlight this in your package&#8217;s README (Postal-Formatter&#8217;s README explicitly notes it includes PHPUnit and PHPStan support ).</p></li></ul><p>Overall, by investing in testing and static analysis, even a small utility library maintains high quality. When you build your own package, consider doing the same &#8211; it pays off in fewer bugs and easier maintenance.</p><div><hr></div><h4>Using PHP 8.1 Enums in Postal-Formatter</h4><p>One particularly interesting aspect of the Postal-Formatter package is its usage of <strong>PHP 8.1 Enums</strong>. Enums (enumerations) are a feature introduced in PHP 8.1 that allow you to define a set of constant values in a type-safe way . Unlike traditional constants or configuration arrays, Enums give you a class-like structure where each possible value is a discrete case of the enum type, and you can even attach methods to it.</p><p>In Postal-Formatter, the CountryCode enum plays a central role. It defines cases for each supported country (like case CZ = 'CZ';, case GB = 'GB';, etc., with the two-letter codes as values). This enum also includes a method formatPostalCode(string $raw): string which contains the logic to format a cleaned postal code for that specific country. Internally, it uses a match expression to apply the correct pattern. For example:</p><ul><li><p>For countries like Czech Republic (CZ) or Slovakia (SK) that use a 3+2 digit format, the enum&#8217;s formatPostalCode returns substr($code,0,3) . ' ' . substr($code,3) (adding a space after the third digit) if the input is 5 digits long.</p></li><li><p>For the UK (GB), it uses a regular expression to split the code into the outward and inward parts (e.g., "SW1A1AA" -&gt; "SW1A 1AA").</p></li><li><p>For Latvia (LV), it prepends "LV-" if the code is 4 digits.</p></li><li><p>And so on for other countries (some use a dash, some have prefixes like AD or MC).</p></li></ul><p>The PostalFormatter::format() function utilizes this enum by attempting to convert the country code string to a CountryCode enum case:</p><pre><code>$country = CountryCode::tryFrom($countryCode) 
    ?? throw InvalidCountryCode::make($countryCode);
return $country-&gt;formatPostalCode($cleaned);</code></pre><p>If tryFrom fails (meaning the provided code isn&#8217;t one of the enum cases), it throws our earlier mentioned InvalidCountryCode exception. Otherwise, it calls the enum&#8217;s formatting method for that country.</p><h4>Why Use an Enum for Country Codes?</h4><p>Enums in PHP 8.1 are a perfect fit when you have a <strong>limited, predefined set of values</strong> &#8212; like country codes. In the case of postal formatting, each country has its own unique rules, so it makes sense to encapsulate those rules alongside the country identifiers themselves.</p><p>By using an enum (CountryCode), we get <strong>type safety by design</strong>: only supported country codes can be used, and any invalid input is caught early. When a user passes a string like "CZ" or "PL", we convert it using CountryCode::tryFrom(), which ensures the value maps to a valid case &#8212; otherwise, an exception is thrown. This avoids the pitfalls of dealing with unchecked strings throughout the codebase.</p><p>Another major advantage is that <strong>each enum case can contain logic</strong>. In this package, we&#8217;ve attached a formatPostalCode() method directly to the CountryCode enum. That means each country case knows how to format its postal code &#8212; no need for a giant switch in a separate service or a hardcoded array of closures. It&#8217;s clean, localized, and easy to extend. Want to support a new country? Just add a new case and its formatting logic. Done.</p><p>This approach also makes the code much easier to <strong>navigate and maintain</strong>. You don&#8217;t have to look across multiple files or layers to understand how formatting works &#8212; it&#8217;s all bundled logically with the enum case itself.</p><p>In short, using an enum here improves:</p><ul><li><p><strong>Clarity</strong> &#8211; the code is more readable and expressive</p></li><li><p><strong>Safety</strong> &#8211; only valid country codes are accepted</p></li><li><p><strong>Extensibility</strong> &#8211; adding new rules is straightforward</p></li><li><p><strong>Modernity</strong> &#8211; this is idiomatic PHP 8.1+, using language features as they were meant to be used</p></li></ul><p>For a utility package like Postal-Formatter, this results in a tidy, robust, and maintainable solution.</p><div><hr></div><h3>Conclusion</h3><p>Building a Composer package in PHP is both rewarding and educational. We started with the motivation &#8211; creating reusable code and sharing it with others &#8211; and walked through the practical steps of publishing a package on Packagist (from setting up your composer.json to tagging releases and submitting to Packagist). Then we explored the Postal-Formatter package as a concrete example of a simple yet useful library. Along the way, we saw how important it is to structure your package well (with proper documentation, autoloading, and licensing) and to uphold code quality via tests and static analysis.</p><p>Finally, we highlighted the use of PHP 8.1 enums in the package, which showcases how embracing new language features can lead to cleaner and more robust solutions. Enums helped ensure only valid country codes are handled and neatly packaged each country&#8217;s formatting logic.</p><p>If you&#8217;re a PHP developer, I encourage you to try creating your own small Composer package. Think of a common problem or utility in your projects, abstract it into a reusable library, and follow the steps to publish it. Not only will it streamline your future development, but you&#8217;ll also get the thrill of contributing to the open-source ecosystem. And if you&#8217;re curious about the Postal-Formatter package, check it out on GitHub or Packagist &#8211; feel free to use it, suggest improvements, or learn from its code. Happy coding, and enjoy your journey into building PHP packages!<br></p><p><strong>Sources:</strong></p><ul><li><p>Official Packagist documentation on <a href="https://packagist.org/about">submitting packages and versioning</a></p></li><li><p>Labrodev Postal-Formatter package on Packagist (README and details)</p></li></ul><p><strong>Picture credits: </strong></p><p><a href="https://unsplash.com/@curology">Unsplash, @curology </a></p><div><hr></div><p></p><p>Thanks for you attention! Subscribe for Labrodev blog to have more interesting articles around web/php/laravel/vue.js development and more. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/subscribe?"><span>Subscribe now</span></a></p><div><hr></div><p>You may also share a post if you found it interesting! </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/p/building-a-simple-php-composer-package?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/p/building-a-simple-php-composer-package?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[FastAPI + Docker for Quick Python APIs]]></title><description><![CDATA[Add lightning-fast Python endpoints to your Laravel app using FastAPI and Docker.]]></description><link>https://labrodev.substack.com/p/fastapi-docker-for-quick-python-apis</link><guid isPermaLink="false">https://labrodev.substack.com/p/fastapi-docker-for-quick-python-apis</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Thu, 26 Jun 2025 16:02:02 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f497e7dd-70aa-44a2-b83b-40a21a5802f9_5184x3456.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever been working with Laravel project when suddenly you need to expose a tiny Python endpoint&#8212;maybe to serve an ML model, process some data, or just run a quick script? You could wrestle with PHP bindings or hack together a CLI call, but there&#8217;s a cleaner, faster way: spin up a microservice with FastAPI and Docker. &#128640;</p><p>Imagine this: your Laravel app doing its thing, not even knowing there&#8217;s a Python service running alongside it. Meanwhile, a lean FastAPI container beside it serves AI model, handles async tasks, or just answers a quick /ping to prove it&#8217;s alive. Let&#8217;s build that today&#8212;in under 10 minutes.</p><div><hr></div><h4>Why FastAPI + Docker</h4><ul><li><p><strong>Separation of concerns.</strong></p><p>We keep our Laravel code in PHP, and all Python-specific logic (whether it&#8217;s ML inference, data transformation, or simple utilities) in a standalone service. No more composer vs. pip drama.</p></li><li><p><strong>Blazing performance &amp; auto-docs.</strong></p><p>FastAPI (powered by Uvicorn) delivers near&#8211;Node.js speeds, full async support, and instantly generates OpenAPI/Swagger docs&#8212;so we spend less time wiring and more time coding.</p></li><li><p><strong>Portable everywhere.</strong></p><p>Docker wraps the entire environment&#8212;Python version, dependencies, configuration&#8212;into a single image. It runs the same on your laptop, in CI, or on Kubernetes. Zero surprises.</p></li></ul><div><hr></div><h4>What we&#8217;ll build in this tutorial</h4><p>As base we will observe <a href="https://github.com/labrodev/fast-api-simple-skeleton">Labrodev&#8217;s Fast API Skeleton. </a></p><p>It gives us as example:</p><ul><li><p>A single <strong>POST /bmi</strong> endpoint to calculate Body Mass Index (BMI)</p></li><li><p>Docker support via a straightforward <strong>Dockerfile</strong></p></li><li><p>A <strong>Makefile</strong> for common build/run/stop/logs/restart commands</p></li><li><p>Instructions for local dev and testing</p></li></ul><p><strong>Project structure </strong></p><pre><code>fast-api-skeleton/
&#9500;&#9472;&#9472; app/
&#9474;   &#9500;&#9472;&#9472; main.py        # FastAPI application
&#9474;   &#9492;&#9472;&#9472; schemas.py     # Pydantic models
&#9500;&#9472;&#9472; Dockerfile         # Container build instructions
&#9500;&#9472;&#9472; Makefile           # Docker lifecycle commands
&#9500;&#9472;&#9472; requirements.txt   # Python dependencies
&#9492;&#9472;&#9472; README.md          # Project documentation</code></pre><p><strong>Requirements </strong></p><ul><li><p><strong>Python 3.9+</strong></p></li><li><p><strong>Docker</strong> (for containerized deployment)</p></li><li><p><em>(Optional)</em> curl or any HTTP client for testing</p><p></p></li></ul><p><strong>Running locally</strong></p><p>Install dependencies:</p><pre><code>pip install --no-cache-dir -r requirements.txt</code></pre><p>Start the FastAPI server:</p><pre><code>uvicorn app.main:app --reload --host 0.0.0.0 --port 8000</code></pre><p></p><p><strong>Running with Docker </strong></p><p>Build the Docker image:</p><pre><code>docker build -t fast-api-skeleton-app .</code></pre><p>Run the container:</p><pre><code>docker run -d --name fast-api-skeleton -p 8000:80 fast-api-skeleton-app</code></pre><p>View logs:</p><pre><code>docker logs -f fast-api-skeleton</code></pre><p>Stop and remove container:</p><pre><code>make stop
make rm</code></pre><p>You may check the Makefile to see other command aliases:</p><pre><code>build:      # Build the Docker image.
&#9;docker build -t fast-api-skeleton-app .

run:        # Run the container (detached).
&#9;docker run -d --name fast-api-skeleton -p 8000:80 fast-api-skeleton-app

stop:       # Stop the running container.
&#9;docker stop fast-api-skeleton

rm:         # Remove the stopped container.
&#9;docker rm fast-api-skeleton

logs:       # Follow container logs.
&#9;docker logs -f fast-api-skeleton

restart:    # Rebuild and restart the container.
&#9;make stop
&#9;make rm
&#9;make build
&#9;make run</code></pre><div><hr></div><h4>Test example</h4><p>So after successful installation and run up container, let&#8217;s try the functionality. <br>Example in Skeleton is Tiny API with POST endpoint /bmi. The goal of this endpoint is to calculate Body Mass Index based on incoming parameters: name, weight, height. </p><p>Let&#8217;s look inside <strong>app/main.py</strong>:</p><pre><code># app/main.py
from fastapi import FastAPI, HTTPException
from .schemas import InputData

app = FastAPI()

@app.post("/bmi")
def calculate_bmi(input_data: InputData):
    # unpack
    w = input_data.weight
    h = input_data.height

    # safety check (Pydantic gt=0 already covers this)
    if h &lt;= 0:
        raise HTTPException(status_code=400, detail="Height must be &gt; 0")

    # BMI formula
    bmi = w / (h * h)
    bmi_rounded = round(bmi, 1)

    return {
        "name": input_data.name,
        "bmi": bmi_rounded,
        "category": interpret_bmi(bmi)
    }

def interpret_bmi(bmi: float) -&gt; str:
    if bmi &lt; 18.5:
        return "Underweight"
    elif bmi &lt; 25.0:
        return "Normal weight"
    elif bmi &lt; 30.0:
        return "Overweight"
    else:
        return "Obesity"</code></pre><p>We could see here in main.py that there is definition of post method with endoiunt /bmi and logic inside it which contains calculation and interpretation of results. Incoming parameters are described in InputData class which is imported and defined in schemas.py.</p><p>And our request schema in <strong>app/schemas.py</strong>:</p><pre><code># app/schemas.py
from pydantic import BaseModel

class InputData(BaseModel):
    name:   str
    weight: float
    height: float</code></pre><p>It&#8217;s quite straightforward to see that in class InputData we describe our BODY parameters and data types of that parameters. </p><p>With this in place, a <strong>POST</strong> to /bmi with:</p><pre><code>{
  "name": "Alice",
  "weight": 70.0,
  "height": 1.75
}</code></pre><p>Returns:</p><pre><code>{
  "name": "Alice",
  "bmi": 22.9,
  "category": "Normal weight"
}</code></pre><p>This skeleton lets you quickly stand up tiny endpoints that send your input straight to an ML model and return just the results you need. You can then call these endpoints from anywhere in your system&#8212;your <strong>Laravel</strong> app, background jobs, or other services&#8212;for seamless, Python-powered inference.</p><div><hr></div><h4>Conclusion </h4><p>We&#8217;ve seen how to spin up a <strong>tiny</strong>, Docker-ready FastAPI service in minutes&#8212;complete with a real /bmi endpoint, auto-generated docs, and a Makefile workflow. That BMI calculator is just a stand-in: we can easily swap in a TensorFlow or PyTorch model by:</p><ol><li><p><strong>Saving</strong> our trained model (e.g. models/my_model.pkl).</p></li><li><p><strong>Mounting</strong> or copying it into the container.</p></li><li><p><strong>Adding</strong> a new endpoint that loads the model and returns results from asking the model</p></li><li><p><strong>Defining</strong> matching Pydantic schemas in app/schemas.py.</p></li></ol><p>From our Laravel application, we simply make an HTTP call&#8212;no messy PHP bindings required. In this setup, FastAPI becomes the bridge between our business logic (Laravel) and any AI model we&#8217;ve built. Tiny, focused, and lightning-fast. &#127881;</p><p>Let&#8217;s give our Laravel projects that Python-powered sidekick!</p><p>Thanks for reading! Subscribe to Labrodev substack and let&#8217;s keep in touch!</p><p>Petro from Labrodev. </p><div><hr></div><p>Picture used in preview credits:<br>Unsplash, <a href="https://unsplash.com/@rocua18">@rocua18 </a></p><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Fail2Ban: A Practical Way to Secure Your Server from Brute Force and Bot Attacks]]></title><description><![CDATA[Instantly shield your Ubuntu 24 server from brute-force logins and automated scans with minimal setup]]></description><link>https://labrodev.substack.com/p/fail2ban-a-practical-way-to-secure</link><guid isPermaLink="false">https://labrodev.substack.com/p/fail2ban-a-practical-way-to-secure</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sat, 24 May 2025 17:19:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cfb6b2e9-b5a9-41b6-9320-788a77403040_4592x3064.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever checked your server logs and noticed a bunch of bizarre login attempts or requests for files that shouldn&#8217;t even be public? If you run a server on the internet, chances are <strong>bots and scripts are constantly poking at it</strong> &#8211; often within minutes of going online. They&#8217;ll try logging in via SSH with random usernames, or scour your website for open directories and sensitive files (like a forgotten <code>.env</code> or <code>.git</code> folder). </p><p>In fact, every website is bombarded with dozens of unwanted requests every day. These automated &#8220;vulnerability scanners&#8221; will casually check if you left a secret key under the doormat &#8211; be it a database dump, an old admin panel, or a default password. As one security researcher put it, <em>&#8220;the wolf has been at your door since the day you put that server online.&#8221;</em> &#128552;</p><p>So, how do we deal with this constant nuisance (and potential danger) of brute-force and bot attacks? Enter <strong>Fail2Ban</strong> &#8211; the unsung hero and bouncer of the server world.</p><h4>What is Fail2Ban and How Does It Help?</h4><p><strong>Fail2Ban</strong> is a lightweight, open-source intrusion prevention framework (written in Python) that protects your server from these automated attacks. Think of it as an automated bouncer for your server: it monitors your log files for suspicious patterns (failed logins, 404 errors from bots scanning for exploits, etc.) and <strong>automatically bans the offending IPs</strong> by adding a rule to your firewall. For example, Fail2Ban will watch something like <code>/var/log/auth.log</code> for repeated SSH login failures and then block that IP address for a while. This dramatically cuts down the noise from bots hammering your services.</p><p>Out of the box, Fail2Ban comes with filters (rules) for many common services &#8211; SSH, FTP, web servers (Apache/Nginx), mail servers, etc. &#8211; and it&#8217;s easily configurable to watch any log file you want. The default configuration alone can stop a lot of bad behavior right away. For instance, the moment some bot throws five wrong passwords at your SSH, Fail2Ban can ban them for 10 minutes or longer (default <em>bantime</em> is 10 minutes, but you can adjust it). The same goes for other protocols: too many FTP login failures, or too many 404 errors on your website, and that IP gets the boot. Essentially, Fail2Ban <strong>reduces malicious login attempts by blocking the source IP addresses</strong> automatically. It&#8217;s like having a security guard who gives any suspicious intruder a time-out.</p><p>Importantly, Fail2Ban isn&#8217;t a silver bullet &#8211; it won&#8217;t stop a <em>distributed</em> attack (where each attempt comes from a different IP), and it doesn&#8217;t fix weak passwords or outdated software. But for the price of a quick install, it adds a very effective layer of defense. It&#8217;s also pretty opinionated software (in a good way): it assumes that if an IP is misbehaving, you&#8217;re better off <strong>not</strong> hearing from it again for a while. And honestly, I agree. &#128521;</p><div><hr></div><h4>Installing Fail2Ban on Ubuntu 24.04</h4><p>Installing Fail2Ban on Ubuntu is straightforward since it&#8217;s available in the official package repository. Here we&#8217;ll focus on Ubuntu 24.04 (or any recent Ubuntu release). Just pop open your terminal and run:</p><pre><code>sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install fail2ban -y 
</code></pre><p>That's it! The Fail2Ban service will start automatically after installation. You can verify it's running by asking it to ping itself:</p><pre><code>sudo fail2ban-client ping</code></pre><p>If everything is OK, it should respond with <code>PONG</code>. &#128994;</p><p><strong>A note on firewalls:</strong> Fail2Ban works by adding rules to your firewall (iptables under the hood). On Ubuntu, many people also use UFW (Uncomplicated Firewall). If UFW is active or if you plan to use it, make sure you have an SSH allow rule <strong>before enabling UFW</strong>, or you might lock yourself out when Fail2Ban bans something. For example:</p><pre><code>sudo ufw allow OpenSSH    
sudo ufw enable           </code></pre><p>Fail2Ban will happily work with UFW as the backend (it supports iptables, UFW, firewalld, etc., automatically). If you&#8217;re not using UFW, Fail2Ban will just use iptables directly. In any case, after installation, Fail2Ban is up and ready to ban bad actors.</p><div><hr></div><h4>Basic Configuration and Enabling the SSH Jail</h4><p>Fail2Ban&#8217;s default settings are decent, but it&#8217;s worth doing a bit of configuration to tailor it to your needs. Configuration is typically done in the file <code>/etc/fail2ban/jail.conf</code> or, better, in a separate <code>jail.local</code> file (to override defaults without getting overwritten on updates). The quick way is to copy the default config to a <code>.local</code> file:</p><pre><code>sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local</code></pre><p>Now open <code>/etc/fail2ban/jail.local</code> in your favorite editor (e.g. <code>sudo nano /etc/fail2ban/jail.local</code>). You&#8217;ll see a bunch of sections for different &#8220;jails.&#8221; A <strong>jail</strong> in Fail2Ban parlance is basically a set of rules for a particular service or pattern (e.g., an SSH jail that watches auth log for SSH failures).</p><p>Let&#8217;s make sure the SSH jail is enabled and tweaked:</p><pre><code>[sshd]
enabled = true        # Make sure this is true to activate the SSH jail
port    = ssh         # or specify the port if you changed it from 22
logpath = /var/log/auth.log
maxretry = 5          # max failed attempts before ban (default 5)
findtime = 10m        # time window for maxretry (default 10 minutes)
bantime  = 1h         # ban duration (e.g. 1 hour; default was 10m)</code></pre><p>In the snippet above, we enable the <code>[sshd]</code> jail. We keep <code>maxretry</code> at 5 (meaning 5 failed login attempts within the <code>findtime</code> window will trigger a ban) &#8211; you can lower this if you want to be stricter. We&#8217;ve increased <code>bantime</code> to 1 hour in this example, instead of the default 10 minutes, because a 10-minute ban is sometimes not much of a deterrent. You could even set <code>bantime = 1d</code> for a full day, or <code>bantime = -1</code> for a <strong>permanent</strong> ban of that IP (though permanent bans might eventually fill up your firewall rules if many accumulate).</p><p>Feel free to adjust these values. For instance, if you rarely ever log in to SSH with a password (as you should be using keys!), you might set <code>maxretry</code> to 3 or even 1 just to ban first-time offenders immediately. The idea is to find a balance &#8211; you want to silence the <strong>noise</strong> of incessant bot attacks without accidentally locking yourself out or banning legitimate users. (Tip: The <code>ignoreip</code> setting under the <code>[DEFAULT]</code> section can whitelist IPs you <em>never</em> want to ban &#8211; e.g. your home/office IP range &#8211; so you don&#8217;t ban yourself by mistake.)</p><p>When you&#8217;re done editing, save the file and restart Fail2Ban to apply changes:</p><pre><code>sudo systemctl restart fail2ban</code></pre><p>Fail2Ban will now be actively monitoring your SSH logs and banning any IPs that violate the rules. &#9989;</p><div><hr></div><h4>Adding a Custom Rule (Blocking Specific Bot Patterns)</h4><p>One of the great things about Fail2Ban is its flexibility. Beyond the pre-packaged jails, you can create your <strong>own rules</strong> to match patterns specific to your environment. Remember those bots looking for <code>.env</code> files or other juicy targets? We can make Fail2Ban ban them too!</p><p>For example, suppose you want to ban any IP that tries to access a hidden file like <code>.env</code> on your web server (because legit users would never do that). We can set up a custom jail for that. Let&#8217;s check it below.</p><h5>Create a filter definition</h5><p>It&#8217;s basically a regex pattern to catch the bad behavior. Filters are stored in <code>/etc/fail2ban/filter.d/</code>. Let&#8217;s create one for &#8220;env file access attempts&#8221;:</p><pre><code>sudo nano /etc/fail2ban/filter.d/custom-env.conf</code></pre><p>Inside this file, define a filter under a <code>[Definition]</code> header. For an Nginx access log, a simple filter to match a <code>.env</code> request might look like:</p><pre><code>[Definition]
failregex = ^&lt;HOST&gt; -.* \"GET \/\.env
ignoreregex =</code></pre><p>Let&#8217;s break that down:</p><ul><li><p><code>&lt;HOST&gt;</code> is a placeholder that Fail2Ban uses to identify the attacker&#8217;s IP from the log line.</p></li><li><p>The regex here is looking for lines where an IP (that&#8217;s the <code>&lt;HOST&gt;</code> at the start of an Nginx log line) requests &#8220;GET /.env&#8221;. We put a backslash before <code>.env</code> because the dot is a special char in regex. This pattern is simplistic (it assumes the request is a GET and doesn&#8217;t fully anchor the line), but it should catch most cases. You can refine regex as needed (check your actual log format!). The <code>ignoreregex</code> is empty, meaning we&#8217;re not defining any pattern to ignore.</p></li></ul><h5>Define the jail using this filter</h5><p>So now open your jail local config (<code>/etc/fail2ban/jail.local</code> or you can create a new file under <code>jail.d/</code> for cleanliness) and add a section for this new jail. For example:</p><pre><code>[nginx-env-scan]
enabled  = true
filter   = custom-env    # refers to the custom-env.conf filter we just created
action   = iptables-multiport[name=NoEnv, port=\"http,https\"]  
logpath  = /var/log/nginx/access.log
maxretry = 1
findtime = 1m
bantime  = 1d</code></pre><p>Here, we named the jail "nginx-env-scan" (name it whatever makes sense to you). We enable it and point it to use the <code>custom-env</code> filter. The <code>action</code> line tells Fail2Ban how to ban (this uses the standard iptables action for blocking on one or multiple ports &#8211; in this case, we block the host on both HTTP and HTTPS ports). We set <code>maxretry = 1</code> and a short <code>findtime</code> of 1 minute, meaning <strong>a single match</strong> is enough to ban the IP for a day. This is a bit aggressive, but for something like <code>.env</code> access attempts, it&#8217;s probably safe &#8211; there are virtually no legitimate reasons for a public user to request that file. (If you were doing this for, say, an <code>/admin</code> URL that <em>could</em> have legit visitors, you&#8217;d use a higher threshold like 3 retries in 5 minutes, etc.)</p><h5>Restart the service</h5><p>Let&#8217;s restart the server to load the new filter and jail:</p><pre><code>sudo systemctl restart fail2ban</code></pre><p>Now Fail2Ban will watch your Nginx access logs and <strong>any IP that tries to GET a </strong><code>.env</code><strong> file will be banned on the spot</strong>. &#127881; Similarly, you could craft rules for other common bot targets &#8211; e.g. <code>wp-login.php</code> (if you&#8217;re not running WordPress, no one should be hitting that), or blocking excessive 404 errors in a short time (indicating someone scanning for lots of files). There are community-contributed filters for common CMS exploits and bad bots, or you can write your own as we just did.</p><p><em>Pragmatic tip:</em> Don&#8217;t go too overboard with hundreds of custom rules for every little thing &#8211; focus on the high-signal patterns (like obvious exploit scans) so you don&#8217;t accidentally ban real users. The goal is to reduce malicious traffic, not to create a self-inflicted denial of service. &#128521;</p><h4>Monitoring Fail2Ban: Status, Logs, and Unbanning</h4><p>Once Fail2Ban is running, you&#8217;ll want to occasionally check in on what it&#8217;s doing. The primary command-line tool is <code>fail2ban-client</code>, which lets you interact with the Fail2Ban daemon.</p><p>Let&#8217;s check some useful commands below.</p><h5>Overall status</h5><p>We may see a summary of jails and overall bans:</p><pre><code>sudo fail2ban-client status</code></pre><h5>Jail status</h5><p>We may get details about a specific jail, including currently banned IPs:</p><pre><code>sudo fail2ban-client status sshd</code></pre><p>This command will show you how many IPs have been banned by the SSH jail and list them. For instance, you might see output indicating X total bans and a list of IP addresses that are currently banned for SSH. Similarly, you can check <code>nginx-env-scan</code> or any other jail by name.</p><p><strong>Logs</strong></p><p>Fail2Ban logs its actions to <code>/var/log/fail2ban.log</code>. It&#8217;s often informative to tail this log to see which IPs are getting banned and why. For example:</p><pre><code>sudo tail -f /var/log/fail2ban.log</code></pre><p>We will see entries whenever a ban or unban happens, e.g., &#8220;Ban xxx.x.xxx.xx on sshd&#8221; etc.</p><p><strong>Unbanning an IP</strong></p><p>Occasionally, we might need to manually unban someone (maybe you banned yourself during testing &#8211; it happens and be <s>careful with that axe, Eugene</s> careful with this tool on your live server!). To unban an IP:</p><pre><code>sudo fail2ban-client set sshd unbanip xxx.x.xxx.xx</code></pre><p>Replace <code>xxx.x.xxx.xx</code> with the IP you want to free from jail. This command tells Fail2Ban to remove that IP from the jail&#8217;s ban list, which in turn removes the firewall rule. After unbanning, that IP can connect again (assuming it fixes its behavior).</p><h5>Banning an IP manually</h5><p>It is also possible to proactively ban an IP (outside the automatic triggers) using a similar command:</p><pre><code>sudo fail2ban-client set sshd banip xxx.x.xxx.xx</code></pre><p>Everything you do with <code>fail2ban-client</code> is also doable by editing config or directly manipulating firewall rules, but the tool provides a nice interface and ensures Fail2Ban&#8217;s internal state stays in sync.</p><div><hr></div><h4>Conclusion: High Impact, Low Effort Security</h4><p>If you&#8217;ve made it this far, you&#8217;ve seen that Fail2Ban offers a <strong>big security win for very little effort</strong>. In about 5 minutes, you installed it and enabled a jail or two, and your server gained the ability to automatically smack down many brute-force and bot-driven attacks. It&#8217;s not fancy, and it&#8217;s certainly not a replacement for good practices (you should still use SSH keys or 2FA, strong unique passwords, keep your software updated, etc.). But Fail2Ban addresses a very common class of threats in a way that&#8217;s both simple and effective. It&#8217;s basically set-and-forget: <em>install, configure once, and let it quietly do its job in the background</em>.</p><p>Personally, I consider Fail2Ban a must-have on any server that faces the wild internet. It&#8217;s like having an invisible force field that <strong>won&#8217;t stop a targeted ninja attack, but will definitely fend off the annoying background noise of the internet&#8217;s riff-raff</strong>. And that means you (and your server) can sleep a little more soundly at night. &#128564;&#128077;</p><p>So go ahead &#8211; give your server that bouncer it deserves. In the eternal words of the security community: <em>&#8220;configure Fail2Ban today, and thank yourself later.&#8221;</em> Stay safe out there!</p><p>Thanks for reading! Subscribe to Labrodev substack and let&#8217;s keep in touch! </p><div><hr></div><p>Picture used in preview credits: <br>Unsplash, <a href="https://unsplash.com/@towfiqu999999">Towfiqu barbhuiya</a></p>]]></content:encoded></item><item><title><![CDATA[Art in Everything: From Bruegel to Web applications]]></title><description><![CDATA[Why creativity is everywhere &#8212; if we choose to see it. In art, in programming code, in our everyday actions. Some thoughts about creativity as the fundamental aspect of human beings.]]></description><link>https://labrodev.substack.com/p/art-in-everything-from-bruegel-to</link><guid isPermaLink="false">https://labrodev.substack.com/p/art-in-everything-from-bruegel-to</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Mon, 24 Feb 2025 09:29:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It is an undeniable fact that Homo Sapiens have engaged in creativity from ancient times to the present. After all, according to the Bible and the Quran, creation itself is the fundamental act that brought the world into being. Creation is both the cause and the prerequisite.</p><p>"Ever-newer waters flow on those who step into the same rivers," said Heraclitus long ago. In this statement, "waters" can be interpreted as the endless and ceaseless generations of people, while "rivers" symbolize the universal essence that unites all who have lived, live now, and will live on Earth. One of the key components of this universal essence is humanity&#8217;s ability to create and interpret creativity.</p><p>A reader of <em>Meditations</em> by Marcus Aurelius is likely to find more thoughts resonating with contemporary times than in the New York Times bestsellers of 2024. The paintings of Pieter Bruegel the Elder evoke emotions no less than those of Banksy. Here and now, in the present, we have Banksy, Stromae, Billie Eilish, and the films of David Lynch&#8212;none of which existed in Bruegel's time. The Flemish school of painting, to which Bruegel belonged, did not exist in the days of Marcus Aurelius. And in Heraclitus&#8217;s lifetime, neither Aurelius nor the Roman Empire had yet emerged. This means that the variety of creative expressions known to humanity accumulates over time, while the universal capacity to understand and interpret creativity not only remains but also expands.</p><p>Yet art has always hidden itself not only in great books and wealthy museums. It is no longer solely "high" or elitist. It can be found everywhere&#8212;in the design of tableware on store shelves, in SpaceX spacecraft and Japanese automobiles, in the kit design of Manchester City for the 2024/25 season, in the latest models of Hoka running shoes. It exists in well-executed work&#8212;whatever that work may be&#8212;in facade renovations, in floral arrangements, in writing code, or in cleaning public restrooms (a nod to <em>Perfect Days</em>). It is present in Chinese souvenirs sold at Istanbul&#8217;s Grand Bazaar and in authentic embroidered shirts at the Kosiv market. It is in the things people craft with their hands&#8212;whether a knitted sweater, a built house, a restored chair, or a necklace of agate. It is in the photographs filling Google and Meta data centers.</p><p>Creativity manifests itself in poetry, paintings, music, and the reflections of modern minds. Of course, not everything deserves a place in a museum or to be immortalized on a vinyl record. But even Duchamp&#8217;s urinal once found its place in world culture. Every act of creation has the right to be named and manifested&#8212;just as it has the right to be ignored or remain unexpressed.</p><p>In the visions of a possible happy future painted by futurists, a common theme emerges: humanity will dedicate most of its time to creativity, education, and science rather than to securing comfortable survival and an illusion of safety. There is an undeniable understanding that the ability and opportunity to create&#8212;without being distracted by financial pyramids, war, or mortgage payments&#8212;will be one of the greatest privileges of the future human.</p><p>In his seminal book <em>Concerning the Spiritual in Art</em>, Wassily Kandinsky wrote:<br><em>"To harmonize the whole&#8212;that is the task of art."</em><br>The whole world is a single entity, and everything around us is subject to harmonization. The further we go, the more complex it becomes, because the number of elements increases. Yet it is within this mosaic that the true imprint of humanity is found.</p><p>And artificial intelligence will not take away humanity&#8217;s ability to create and interpret creativity. Because, as it seems, this is a gift granted to humans by the very force that once, through an act of creation, made them in its own image.</p><div><hr></div><p>And here are some beautiful paintings which I&#8217;ve enjoyed in my recent trip to Vienna.</p><ol><li><p><strong>Pieter Bruegel the Elder - The Hunters in the Snow (1565) </strong></p><p><em>Kunsthistorisches Museum</em></p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ks77!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ks77!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ks77!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ks77!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ks77!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ks77!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg" width="960" height="1280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:141086,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/157795322?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ks77!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ks77!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ks77!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ks77!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F47fa5470-9f45-488c-9d7e-b9969329ecdc_960x1280.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol start="2"><li><p><strong>Egon Schiele - Self-portrait with stripped shirt (1910)</strong></p><p><em>Leopold Museum</em></p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!2TM9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!2TM9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!2TM9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg" width="960" height="1280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:101699,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/157795322?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!2TM9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!2TM9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc656131e-2e6e-458c-9e11-beedcf25c770_960x1280.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><ol start="3"><li><p><strong>Max Pechstein - Young Lady with Feather Hat (1910)</strong></p><p><em>Leopold Museum</em></p></li></ol><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!g1yL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!g1yL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!g1yL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg" width="960" height="1280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:161002,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/157795322?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!g1yL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!g1yL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feafb537c-cd75-4499-8d6b-56bf8fc75eff_960x1280.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And the beautiful quote from Egon Schiele:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!apGP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!apGP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!apGP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!apGP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!apGP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!apGP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg" width="960" height="1280" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1280,&quot;width&quot;:960,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:204142,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://labrodev.substack.com/i/157795322?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!apGP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 424w, https://substackcdn.com/image/fetch/$s_!apGP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 848w, https://substackcdn.com/image/fetch/$s_!apGP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!apGP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff92e1cef-2862-47c0-9873-4afbbeee142a_960x1280.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p>]]></content:encoded></item><item><title><![CDATA[Leveraging Traits in Laravel Eloquent Models]]></title><description><![CDATA[Building Reusable Functionality with the Numberable Package]]></description><link>https://labrodev.substack.com/p/leveraging-traits-in-laravel-eloquent</link><guid isPermaLink="false">https://labrodev.substack.com/p/leveraging-traits-in-laravel-eloquent</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sun, 10 Nov 2024 12:39:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/dcb625ce-4730-4d45-b540-29fab0b6c032_2070x1380.avif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Laravel development, traits are incredibly powerful tools that allow you to add reusable functionality to Eloquent models. Whether it's automating attribute generation or customizing model behavior, traits help streamline complex functionality across models while keeping your codebase clean and maintainable.</p><p>In this article, we&#8217;ll explore how traits work in Laravel, how they can be used to add shared functionality to Eloquent models, and dive into an example with a custom <code>Numberable</code> package&#8212;a trait-based package for generating unique, consistent document numbers for models. We&#8217;ll also touch on how to separate a trait into a standalone Laravel package, making it reusable across projects.</p><div><hr></div><h4>What Are Traits and Why Use Them?</h4><p>Traits in PHP are a mechanism for code reuse, allowing classes to inherit methods from multiple sources. In Laravel, traits are often used to add reusable methods or functionalities to Eloquent models. Instead of creating a complex inheritance hierarchy, you can simply import a trait, making your model gain additional behaviors or features without impacting the rest of the application.</p><p><strong>Example Uses of Traits</strong>:</p><ul><li><p><strong>Automatic Timestamps</strong>: Custom traits can automatically set timestamps for specific actions.</p></li><li><p><strong>Soft Deletes</strong>: Laravel&#8217;s <code>SoftDeletes</code> trait allows models to be marked as deleted without removing them from the database.</p></li><li><p><strong>Custom Attributes</strong>: Traits can add calculated or formatted attributes for models.</p></li></ul><p>Traits are an excellent choice for sharing functionality across different models, especially when you need consistency and reusability. They allow us to keep our models lean and focused on their primary business logic.</p><div><hr></div><h4>Building the Numberable Trait: An Example</h4><p>Our <code>Numberable</code> package demonstrates how traits can add reusable functionality to Eloquent models. This package provides a trait, <code>ModelHasNumber</code>, which automatically assigns a unique document number to each model when it&#8217;s created.</p><p>The generated number format can be customized, making this package ideal for any application that needs consistent, flexible document numbering. Here&#8217;s a brief overview of how the <code>ModelHasNumber</code> trait works.</p><pre><code>// Labrodev\Numberable\ModelHasNumber.php

namespace Labrodev\Numberable;

use Illuminate\Support\Str;
use Carbon\Carbon;

trait ModelHasNumber
{
    public static function bootModelHasNumber(): void
    {
        static::created(function ($model) {
            if (!$model-&gt;isModelHasNumberTraitValueGenerated()) {
                if (isset($model-&gt;id)) {
                    $model-&gt;{$model-&gt;modelHasNumberTraitColumn()} = $model-&gt;generateNumberByTraitModelHasNumber($model-&gt;id);
                    $model-&gt;withoutEvents(function () use ($model) {
                        $model-&gt;save();
                    });
                }
            }
        });
    }

    protected function modelHasNumberTraitColumn(): string
    {
        return 'number';
    }

    private function isModelHasNumberTraitValueGenerated(): bool
    {
        $name = $this-&gt;modelHasNumberTraitColumn();
        return isset($this-&gt;attributes[$name]) &amp;&amp; $this-&gt;attributes[$name] !== null;
    }

    protected function generateNumberByTraitModelHasNumber(int $modelId): string
    {
        $currentYear = Carbon::now()-&gt;year;
        $predictionNumber = $this-&gt;predictionNumberForTraitModelHasNumber();
        $paddingLength = strlen((string) $predictionNumber);

        return $currentYear . Str::padLeft((string) $modelId, $paddingLength, '0');
    }

    protected function predictionNumberForTraitModelHasNumber(): int
    {
        return 10000;
    }
}
</code></pre><h4>How <code>ModelHasNumber</code> Works</h4><ul><li><p><strong>Automatic Number Generation</strong>: The trait attaches a listener to the <code>created</code> event of the model. Upon creation, it checks if the number attribute is already set, and if not, it generates a new document number.</p></li><li><p><strong>Year-Based Numbering</strong>: The number generated includes the current year as a prefix, followed by a padded model ID. This format is useful for document numbers, invoice numbers, or order IDs.</p></li><li><p><strong>Customizable Logic</strong>: You can override the <code>generateNumberByTraitModelHasNumber</code> method within a model to provide a custom document number format. This allows you to adapt the numbering system for specific requirements.</p></li></ul><p><strong>Example Number Format</strong>: If the current year is 2024, the model ID is <code>45</code>, and the prediction number is <code>10000</code>, the document number generated would be <code>202400045</code>. This ensures a consistent document number length.</p><div><hr></div><h4>Customizing the Trait in Models</h4><p>Laravel makes it easy to override trait methods within a model. If a model requires a custom numbering format, you can simply override the <code>generateNumberByTraitModelHasNumber</code> method to create a unique numbering scheme without modifying the base trait.</p><pre><code>use Labrodev\Numberable\ModelHasNumber;

class Order extends Model
{
    use ModelHasNumber;

    protected function generateNumberByTraitModelHasNumber(int $modelId): string
    {
        // Custom numbering logic for Order model
        $prefix = 'ORD';
        return $prefix . Carbon::now()-&gt;year . str_pad((string) $modelId, 6, '0', STR_PAD_LEFT);
    }
}
</code></pre><p>In this example, the <code>Order</code> model will have a custom numbering format, such as <code>ORD2024000045</code>, combining a prefix with the year and a padded ID.</p><div><hr></div><h4>Packaging the Trait as a Laravel Package</h4><p>Creating a standalone package for the <code>ModelHasNumber</code> trait allows it to be reused across multiple projects. Here&#8217;s how to set up this package in a few steps:</p><ol><li><p><strong>Create the Package Structure</strong>: Inside your <code>packages</code> directory, create a folder for your package, for example, <code>Labrodev/Numberable</code>, with a <code>src</code> folder containing the <code>ModelHasNumber.php</code> trait.</p></li><li><p><strong>Set Up </strong><code>composer.json</code>: In the root of the package folder, create a <code>composer.json</code> file to define the package&#8217;s dependencies and metadata:</p></li></ol><pre><code>{
    "name": "labrodev/numberable",
    "description": "A Laravel package for automatic document numbering",
    "type": "library",
    "require": {
        "php": "&gt;=8.1",
        "illuminate/contracts": "^10.0",
        "nesbot/carbon": "^2.72",
        "ramsey/uuid": "^4.7"
    },
    "require-dev": {},
    "autoload": {
        "psr-4": {
            "Labrodev\\Numberable\\": "src/"
        }
    },
    "scripts": {
        "post-autoload-dump": "@composer run prepare",
        "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
        "prepare": "@php vendor/bin/testbench package:discover --ansi",
        "build": [
            "@composer run prepare",
            "@php vendor/bin/testbench workbench:build --ansi"
        ],
        "start": [
            "Composer\\Config::disableProcessTimeout",
            "@composer run build",
            "@php vendor/bin/testbench serve"
        ]
    },
    "config": {
        "sort-packages": true
    },
    "minimum-stability": "dev",
    "prefer-stable": true
}</code></pre><div><hr></div><h3>Conclusion</h3><p>Traits are a fantastic way to add modular functionality to Eloquent models, making it easy to reuse logic across different models. The <code>Numberable</code> package is a practical example of how traits can encapsulate specific functionality, in this case, document number generation. By converting it into a standalone package, we&#8217;ve made it possible to reuse this functionality across Laravel projects with ease.</p><p>Traits are especially useful when combined with Laravel&#8217;s event-driven model lifecycle, allowing us to automate processes like assigning unique numbers to models. With a little customization, traits like <code>ModelHasNumber</code> can serve a variety of business needs, keeping your Laravel codebase clean, consistent, and highly maintainable.</p><p>For the full implementation, check out our <a href="https://github.com/labrodev/laravel-numberable">laravel-numberable package on GitHub</a>.</p><div><hr></div><p><strong>Thanks for reading!</strong></p><p>Subscribe to our Substack to get more tips and tutorials on <strong>Laravel</strong> development and best practices delivered straight to your inbox.</p><div><hr></div><p>Preview photo credit:</p><p><a href="https://unsplash.com/@francesco_ungaro">https://unsplash.com/@francesco_ungaro</a></p>]]></content:encoded></item><item><title><![CDATA[UUID Generation in Eloquent Models in Laravel]]></title><description><![CDATA[Integrate UUIDs into Laravel Models for Unique and Reliable Identifiers using Trait approach]]></description><link>https://labrodev.substack.com/p/uuid-generation-in-eloquent-models</link><guid isPermaLink="false">https://labrodev.substack.com/p/uuid-generation-in-eloquent-models</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sun, 23 Jun 2024 18:55:04 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/371002fb-bc04-487c-8f3b-776a13242f51_9504x6336.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>UUIDs (Universally Unique Identifiers) are a fantastic way to give your database records unique keys. Unlike auto-incrementing IDs, UUIDs are almost guaranteed to be unique, even across different databases and instances. This makes them perfect for distributed systems or when you need to merge databases. </p><p>They can also be really useful in APIs, where they can serve as unique resource identifiers. As well as in Dashboards or Back end interfaces where we could use them as route parameters instead of plain old IDs.</p><p>In this article, we&#8217;ll describe how to  implement UUID generation for Eloquent models in Laravel using a reusable trait.</p><h5>Step 1: Add UUID column in table in database</h5><p>First, you need to add a UUID column to your table in the database (<em>if your table is already has uuid column, skip this step</em>). You can do this by creating a new migration or modifying an existing one. Here's an example of Laravel migration how to add a UUID column to a table:</p><pre><code>public function up()
{
    Schema::table('example_table', function (Blueprint $table) {
        $table-&gt;uuid('uuid')-&gt;unique()-&gt;nullable();
    });
}

public function down()
{
    Schema::table('example_table', function (Blueprint $table) {
        $table-&gt;dropColumn('uuid');
    });
}
</code></pre><h5>Step 2: Create the Trait</h5><p>Let&#8217;s create a trait that automatically generates and assigns a UUID when a new Eloquent model is created.</p><pre><code>&lt;?php

declare(strict_types=1);

namespace Labrodev\Uuidable;

use Illuminate\Support\Str;

trait ModelHasUuid
{
    /**
     * Boot the trait and add the creating event listener.
     *
     * @return void
     */
    public static function bootModelHasUuid(): void
    {
        static::creating(function ($model) {
            /** @var static|self $model */
            if (empty($model-&gt;{$model-&gt;fetchUuidColumn()})) {
                $model-&gt;{$model-&gt;fetchUuidColumn()} = Str::uuid();
            }
        });
    }

    /**
     * Get the name of the UUID column.
     *
     * @return string
     */
    protected function fetchUuidColumn(): string
    {
        return 'uuid';
    }
}</code></pre><p>This trait hooks into the <code>creating</code> event of the model and assigns a UUID if the UUID column is empty. The <code>fetchUuidColumn</code> method specifies the column name used for storing the UUID. If your column is named differently, you can override this method in your model.</p><p>When we use the <code>bootModelHasUuid</code> static function, it seamlessly integrates with Laravel Eloquent's booting mechanism. Laravel automatically calls the <code>boot</code> method on all traits used by a model, allowing the <code>bootModelHasUuid</code> method to contribute its functionality to the model's primary boot method. This ensures that the UUID assignment logic is consistently applied whenever a new model instance is created.</p><h5>Step 3: Using the Trait in a Model</h5><p>To use the <code>ModelHasUuid</code> trait, include it in your Eloquent model like this:</p><pre><code>&lt;?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Labrodev\Uuidable\ModelHasUuid;

class ExampleModel extends Model
{
    use ModelHasUuid;
}
</code></pre><p><strong>Step 4: Customizing the UUID Column Name</strong></p><p>If your UUID column is named something other than <code>uuid</code>, you can override the <code>fetchUuidColumn</code> method in your model:</p><pre><code>&lt;?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Labrodev\Uuidable\ModelHasUuid;

class ExampleModel extends Model
{
    use ModelHasUuid;

    /**
     * Override method to specify a custom column name.
     *
     * @return string
     */
    protected function fetchUuidColumn(): string
    {
        return 'custom_uuid_column';
    }
}</code></pre><h5>Conclusion</h5><p>By following these steps, you can implement UUID generation for Eloquent models in Laravel, providing a reliable and unique identifier for your records. This approach is encapsulated in a reusable trait, making it easy to apply to any model within your application. For the full implementation, check out our <a href="https://github.com/labrodev/laravel-uuidable">laravel-uuidable package on GitHub</a>.</p><div><hr></div><p><strong>Thanks for reading!</strong></p><p>Subscribe to our Substack to get more tips and tutorials on <strong>Laravel</strong> development and best practices delivered straight to your inbox.</p><div><hr></div><p>Preview photo credit: </p><p><a href="https://unsplash.com/@afgprogrammer">https://unsplash.com/@afgprogrammer</a></p>]]></content:encoded></item><item><title><![CDATA[SSH Brute force attacks: Obvious way to secure your server]]></title><description><![CDATA[Learn about SSH brute force attacks and discover an effective way to secure your remote server from such threats by using SSH key authentication instead of login/password authentication.]]></description><link>https://labrodev.substack.com/p/ssh-brute-force-attacks-obvious-way</link><guid isPermaLink="false">https://labrodev.substack.com/p/ssh-brute-force-attacks-obvious-way</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Mon, 03 Jun 2024 16:47:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/359d918f-9e2c-4ec0-a9bf-07e0e142f5ca_7952x5304.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Introduction </h4><p>I want to cover a straightforward topic about using SSH key authentication to access your remote server as a preventive measure against unauthorized access in your environment. This article doesn't contain any big insights, and I'm not a cybersecurity professional, so this is not an article to cover the depths of securing remote servers (nor does it dig deep into discussing the insecure nature of SSH in general).</p><p> I'm just a software developer who operates a bunch of remote servers (mostly Ubuntu, VPS). These servers host many projects&#8212;test, live, commercial, educational, and demo&#8212;with numerous .env files. These are things you always want to keep private for yourself, your clients, and your colleagues.</p><p>Here are some first-hand measures that can at least reduce potential risks in case someone discovers your login/password and connects to your server through SSH. </p><p>And now, let's talk about <strong>SSH brute force attacks</strong>.</p><p>If you check the access logs of some of your servers that host indexed websites or are exposed to public domains through DNS, you might find numerous attempts to log in to your server from random IP addresses (often from unexpected geo-locations). </p><p>You're lucky that none of these attempts succeeded and were disconnected due to authentication failures (incorrect login/password combinations). However, statistically, one pair could match one day, and that's a problem. It's even easier if your login is named "root," isn't it?</p><p>So here you may find in your server access logs:</p><pre><code>11:40:50.181
Dec 22 12:40:50 your-server sshd[4922]: Disconnected from authenticating user root xxx.xxx.xx.xxx port 41112 [preauth]
 
11:41:38.002
Dec 22 12:41:38 your-server sshd[5050]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=xxx.xxx.xx.xx user=root
 
11:41:39.751
Dec 22 12:41:39 your-server sshd[5050]: Failed password for root from xxx.xxx.xx.xx port 49826 ssh2
 
11:41:39.904
Dec 22 12:41:39 your-server sshd[5052]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=xxx.xxx.xx.xx  user=root
 
11:41:39.940
Dec 22 12:41:39 your-server sshd[5050]: Received disconnect from xxx.xxx.xx.xx port 49826:11: Bye Bye [preauth]
 
11:41:39.940
Dec 22 12:41:39 your-server sshd[5050]: Disconnected from authenticating user root xxx.xxx.xx.xx port 49826 [preauth]
 
11:41:41.593
Dec 22 12:41:41 your-server sshd[5052]: Failed password for root from 179.95.180.141 port 53538 ssh2
 
11:41:41.787
Dec 22 12:41:41 your-server sshd[5052]: Received disconnect from xxx.xxx.xx.xx port 53538:11: Bye Bye [preauth]
 
11:41:41.787
Dec 22 12:41:41 your-server sshd[5052]: Disconnected from authenticating user root xxx.xxx.xx.xx port 53538 [preauth]

11:41:47.612
Dec 22 12:41:47 your-server sshd[5058]: Invalid user wyg from xxx.xxx.xx.xx port 52864</code></pre><p>As you can see, there are different attempts from random IP addresses (imagine any random IP address instead of xxx.xxx.xx.xx) to connect to your server through SSH. This is how SSH brute force attacks look.</p><h4>Understanding SSH Brute force attacks</h4><p>A brute force attack is a method used by attackers to gain unauthorized access to accounts by systematically trying all possible password combinations. This approach can be incredibly effective if passwords are weak or not protected by additional security measures.</p><p>Many brute force attacks are automated using bots. Attackers deploy these bots across the internet to scan for servers with open SSH ports. Once a potential target is found, the bots use a list of commonly used usernames and passwords to attempt to log in. This method allows attackers to try thousands of combinations in a short period, increasing their chances of success.</p><h4>Solution </h4><p>To defend your server from these attacks, obvious solution is to replace SSH login/password authentication with SSH key authentication.</p><p>SSH keys provide a more secure alternative to password authentication, eliminating the chance for bots to quickly find the correct login/password combination.</p><p>Let&#8217;s go through step-by-step explanation how to make this twist. </p><p><em>Disclaimer: My local environment is Ubuntu (so keep in mind if you are using another operation system, which handle ssh key generation not in the same way as in Ubuntu, just find additionally how to do it with your setup).</em></p><p><strong>Step 1: Generate SSH key-pair on your local </strong></p><p><strong>1. Open your terminal and input</strong></p><pre><code>ssh-keygen</code></pre><p><strong>2. Follow the prompts </strong></p><ul><li><p>You will be asked where to save the key. Press <code>Enter</code> to accept the default location (<code>/home/user/.ssh/id_rsa</code>).</p></li><li><p>Enter a passphrase for additional security, or press <code>Enter</code> to leave it empty.</p></li></ul><p>You will receive as output something like this:</p><pre><code>Your identification has been saved in /home/user/.ssh/id_rsa
Your public key has been saved in /home/user/.ssh/id_rsa.pub
The key fingerprint is:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (some real data here)
The key's randomart image is:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (some real data here)</code></pre><p><strong>Step 2: Copy generated public ssh key on your server</strong></p><p><strong>1. Input in terminal</strong></p><pre><code>ssh-copy-id your-server-username@your-server-ip</code></pre><ul><li><p>Replace <code>your-server-username</code> with your actual username on the server (which is configured to login on server through ssh)</p></li><li><p>Replace <code>your-server-ip</code> with the server's IP address or host name</p></li></ul><p><strong>2. Enter Your Password</strong></p><p>You will be prompted to enter your password for the server. </p><p>As output of this command you may receive something like this as output:</p><pre><code>Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'your-server-username@your-server-ip'"
and check to make sure that only the key(s) you wanted were added.</code></pre><p>And after these steps, you will be able to log in through SSH key authentication from your local machine to your remote server without needing to enter a password anymore.</p><p><strong>3. Ensure that new  SSH key authentication works correctly</strong></p><p>Input in your console</p><pre><code>ssh your-server-username@your-server-ip</code></pre><p><em><strong>Important</strong></em></p><p>And only if you are able to log in to your server through this command, you may move forward to the second part of the process and disable login/password authentication in the server configuration (let&#8217;s explore this in step 4 below). It's important because if you will disable password authentication on your server, but your ssh-key authentication doesn&#8217;t work for some reasons, then you will loose access to your own server. </p><p><em>Careful with that axe, Eugene!</em></p><p><strong>Step 3: Disable Password Authentication</strong></p><p><strong>1. Log in into your remote server</strong></p><pre><code>ssh your-server-username@your-server-ip</code></pre><p><strong>2. Open the SSH Configuration File</strong></p><pre><code>sudo nano /etc/ssh/sshd_config</code></pre><p><strong>3. Find and Edit PasswordAuthentication Directive</strong></p><p>Change the line <code>#PasswordAuthentication yes</code> to:</p><pre><code><code>PasswordAuthentication no</code></code></pre><p>Press <code>Ctrl + X</code>, then <code>Y</code>, and <code>Enter</code> to save and exit.</p><p><strong>4. Restart the SSH Service</strong></p><pre><code><code>sudo systemctl restart sshd</code></code></pre><p><strong>Step 4: Confirm changes </strong></p><p><strong>1. Once again try to log in to your remote server from local with ssh-key </strong></p><pre><code>ssh your-server-username@your-server-ip</code></pre><p>You should successfully log in.</p><p><strong>2. Try to login/password authentication</strong></p><pre><code><code>ssh your-server-username@your-server-ip -o PubkeyAuthentication=no</code></code></pre><p>And you should see an error indicating that password authentication is not allowed which is our expected behavior. </p><p><strong>Step 5: Backup your SSH private key </strong></p><p>Ensure your private SSH key (<code>home/user/.ssh/id_rsa</code>) is backed up securely (the path to <code>id_rsa </code>is provided here as an example and may vary based on your setup). Keep it in a secure location, not only on your local computer, as it&#8217;s a crucial part of SSH key authentication. If you lose your key, you may have trouble logging into your remote server.</p><h4>Conclusion </h4><p>In this article, we&#8217;ve highlighted the problem of SSH brute force attacks - how they are done, how to identify if your server is struggling with them, and what the solution is.</p><p>The solution is obvious and straightforward - use SSH key authentication instead of login/password authentication.</p><p>You may find here a step-by-step guide on how to implement this solution.</p><p>Enjoy your enhanced security and don&#8217;t give bots a chance to find the back door into your servers.</p><p>Thank you for your attention!</p><div><hr></div><p><em>Picture in preview credit: </em></p><p><em>Unsplash, <a href="https://unsplash.com/@flyd2069">FlyD </a></em></p><p></p>]]></content:encoded></item><item><title><![CDATA[Using Static Analysis in Laravel: A Guide to Starting with PHPStan in Your Project]]></title><description><![CDATA[Step-by-Step Guide on How to Install and Configure PHPStan in Laravel to Begin Benefiting from Code Coverage with Static Analysis]]></description><link>https://labrodev.substack.com/p/using-static-analysis-in-laravel</link><guid isPermaLink="false">https://labrodev.substack.com/p/using-static-analysis-in-laravel</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sat, 27 Jan 2024 13:10:45 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a1c9b39d-0978-4dde-963f-6ff5707ec5dd_600x600.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's always beneficial to explore ways to improve code, particularly in maintaining consistent formatting and readability, especially in larger projects. That's why it's crucial to consider using static analysis in your project if you haven't already.</p><p>Static analysis tool in PHP is a game-changer for code quality and readability. These tools are like having an extra pair of eyes that meticulously scan your code, pointing out any errors, typos, or even potential issues that could turn into bigger problems down the line. This is super handy because, let's face it, mistakes happen more often than we'd like to admit. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Labro:Dev! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><strong><a href="https://phpstan.org/">PHPStan</a></strong> is one of the most popular and free static analysis tools. So in this post, I just want to briefly provide a step-by-step guide on how to use PHPStan in a <strong>Laravel </strong>project. I'm hoping this will be particularly useful for those who are new to this topic.</p><p><strong>1. Install <a href="https://github.com/phpstan/phpstan">PHP Stan </a></strong></p><pre><code>composer require phpstan/phpstan --dev</code></pre><p><strong>2. Install Larastan </strong></p><pre><code>composer require nunomaduro/larastan --dev</code></pre><p><strong><a href="https://github.com/larastan/larastan">Larastan</a></strong> is specifically designed for Laravel projects. It's essentially an extension of PHPStan. By using Larastan in your Laravel projects, you're essentially adding an extra layer of quality control. It helps ensure that your code is not just error-free, but also adheres to good coding practices specific to Laravel. It provides the static analysis tool with an understanding of most of Laravel's beautiful magic.</p><p><strong>3. Install Laravel IDE Helper </strong></p><pre><code>composer require --dev barryvdh/laravel-ide-helper</code></pre><p><strong><a href="https://github.com/barryvdh/laravel-ide-helper">Laravel IDE Helper Generator for Laravel</a></strong> is package to generate PHPDocs, mostly for Laravel models. </p><p><strong><a href="https://phpstan.org/writing-php-code/phpdocs-basics">PHPDocs</a></strong> play a significant role in describing code to static analysis tools. Therefore, it's highly beneficial to leverage this package and ensure your models are thoroughly documented with essential PHPDocs.</p><p>It also generates helper files that enable your IDE to provide accurate autocompletion. Generation is done based on the files in your project, so they are always up-to-date. </p><p><strong>4. Configure Laravel IDE Helper </strong></p><p>Let&#8217;s publish configuration file of package to project config directory. </p><pre><code>php artisan vendor:publish --provider="Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider" --tag=config</code></pre><p>After this command, you may find <strong>ide-helper.php </strong>file in <strong>config</strong> folder. </p><p>Changing any of configuration there is optionally, but I want to ended up with one thing there: </p><pre><code>'model_locations' =&gt; [
    'app',
 ],</code></pre><p>You may adjust locations of your models. For example, if some of your models are located in your core package in vendor, you may just provide exact location here, like this: </p><pre><code>'model_locations' =&gt; [
    'app',
    'vendor/labrodev/filter-components/src/Models'
],</code></pre><p><strong>5. Generate Laravel IDE Helper files </strong></p><p><strong>Generate ide-helper file</strong></p><pre><code>php artisan ide-helper:generate </code></pre><p>This command is used to generate a file that provides correct PHPDoc annotations for all Facade classes. This helps integrated development environments (IDEs) understand the Laravel Facades better, thereby improving auto-completion and code analysis (and static analysis) features. </p><p>As a result, you may find generated file <strong>_ide_helper.php </strong>in root of your project.</p><p><strong>Add \Eloquent mixin</strong></p><pre><code>php artisan ide-helper:eloquent</code></pre><p>This command adds docblock mixin to /vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:</p><pre><code>@mixin \Eloquent</code></pre><p>This annotation tells the IDE that the methods from the Eloquent class are available in the model, even though they're not explicitly defined there. This enables the IDE to offer code completion, function lookup, and other helpful insights for Eloquent methods when you're working on your models. And again, it helps to better understanding of code and static analysis.</p><p><strong>Generate PHPDocs for models </strong></p><p>There is a 2 ways of adding PHPDocs in your model.</p><ul><li><p>to add PHPDocs to each Model class directly</p></li><li><p>to generate separate files with PHPDocs for each Model class with mixin alias and to put only this alias to each Model file.</p></li></ul><p>From my perspective, the second approach is much better because it avoids overloading models with lengthy PHPDoc blocks that don't add any functional value. Instead, everything necessary for static analysis is kept in one separate, specific file.</p><p>To generate PHPDocs in a separate file and add only mixins to the Model classes, we need to use the <code>--write-mixin</code> option in the command. Conversely, to write PHPDocs directly into Model classes, use the <code>--write</code> option.</p><p><em>More information specifically related to this command you may find <a href="https://github.com/barryvdh/laravel-ide-helper?tab=readme-ov-file#automatic-PHPDocs-for-models">here</a>.</em></p><pre><code>php artisan ide-helper:models --write-mixin</code></pre><p>As result you may find generated file <strong>_ide_helper_models.php</strong> with all PHPDocs related to each model. </p><p>And you may check your Model classes, there you may find mixin with alias to _ide_helper_models classes. Something like this: </p><pre><code>/**
 * @mixin IdeHelperYourModel
 */</code></pre><p><strong>Generare PHPStorm meta information</strong></p><p>It&#8217;s optionally and could give benefits mostly for your IDE environment if IDE is PHP Storm.</p><pre><code>php artisan ide-helper:meta</code></pre><p>As result, you may find <strong>.phpstorm.meta.php </strong>file at the root of your project.</p><p><strong>6. Create PHPStan configuration file </strong></p><p>Let&#8217;s create phpstan.neon file in root of your project. And adjust configuration of PHPStan.</p><pre><code>includes:
    - ./vendor/nunomaduro/larastan/extension.neon
parameters:
    level: 9
    paths:
        - app
    scanFiles:
        - _ide_helper.php
        - _ide_helper_models.php
        - .phpstorm.meta.php
    checkGenericClassInNonGenericObjectType: false</code></pre><p>Let&#8217;s break up what&#8217;s going on in this configuration file. </p><ul><li><p><strong>includes: </strong>specifies that the Larastan configuration will include settings from the <code>extension.neon</code> file found in the Larastan package. This file typically contains Larastan-specific configurations and is essential for integrating Larastan's static analysis capabilities into your project.</p></li><li><p><strong>level: 9</strong>: This sets the strictness level of the analysis. In PHPStan (and Larastan by extension), the levels range from 0 (most lenient) to 9 (most strict). A higher level means the tool will perform a more thorough analysis, catching more potential issues.</p></li><li><p><strong>paths: </strong>this tells Larastan to analyze the files in the <strong>app</strong> directory of your Laravel project. It's where most of your application's core logic resides, so it's crucial for static analysis.</p></li><li><p><strong>scanFiles: </strong> files are added to the scan list to ensure that Larastan understands your IDE helpers and any custom meta-information provided by PHPStorm (or other IDEs). It helps in making the analysis more accurate, especially for Laravel-specific features and helpers.</p></li><li><p><strong>checkGenericClassInNonGenericObjectType</strong>: this option, when set to false, instructs Larastan to not report errors related to the usage of generic classes in non-generic object types. </p></li></ul><p><strong>7. Define composer command to run PHP Stan analysis </strong></p><p>Let&#8217;s go to composer.json and adjust &#8220;scripts&#8221; part to define command for running PHP Stan from composer. </p><pre><code>"scripts": {
    "phpstan": "vendor/bin/phpstan analyse"
},</code></pre><p>So now we may use this command in console to run PHP Stan:</p><pre><code>composer phpstan </code></pre><p>Now, you may enjoy results of static analysis and investigate reports with errors provided by PHP Stan. </p><p>Wish you happy coding and green reports in PHP Stan. </p><p>Thanks you for attention. </p><p></p><p></p><p></p><p></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Labro:Dev! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Resolving global variable conflicts in Minified JavaScript: A Case Study with Vite and Terser]]></title><description><![CDATA[Simple Solutions to Avoid Global Variable Conflicts in Minified JavaScript Using Vite and Terser]]></description><link>https://labrodev.substack.com/p/resolving-global-variable-conflicts</link><guid isPermaLink="false">https://labrodev.substack.com/p/resolving-global-variable-conflicts</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Wed, 10 Jan 2024 18:25:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7b954d43-6d84-4791-9610-61cae93a8185_1000x1000.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Case</h3><p>Recently, I've faced an issue that my JavaScript library, which my clients use on their site, doesn't work because there is a conflict of global variables, as another JavaScript code uses the same global variable as mine.</p><p>Of course, this is a common problem, especially when multiple scripts are included on a web page. It&#8217;s well-known that JavaScript is very flexible with its global namespace, which means any variable declared without <code>var</code>, <code>let</code>, or <code>const</code> (or declared with <code>var</code> in the global scope) becomes a global variable. If two different scripts declare the same global variable, they will conflict with each other. If some of external JavaScript libraries are using a variable with the same name in the global scope, one script's variable will overwrite the variable from the other script. This can cause unexpected behavior or errors, as one of the libraries might not work as intended with the overwritten variable. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Labro:Dev! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>My code doesn't work in this scenario because it loads into the DOM after another library that uses the same naming convention. To resolve this issue, the first step is to rename my variable, verify if it's necessary in the global scope, or consider encapsulating it within namespaces, among other solutions. </p><p>The problem is that my library consists of minified JS code, which essentially contains an entire single-page application from Vue.js 3. Therefore, my conflicting global variable is a shorter name of something else, which acquired its naming during the mangling process that occurs during code compilation and minification. </p><p>Let&#8217;s breakdown what is <strong>mangling. </strong>In the context of JavaScript minification, the "mangling" process is a specific step that involves renaming variables and function names to shorter ones. It helps to reduce the overall length of code and size of code file. It&#8217;s a part of minification. So, for example, variable named <code>userInput</code> might be renamed to <code>a</code>.</p><h3>Solution </h3><p>Directly and brutally renaming a conflicting variable in a minified JS file is not a good idea, as it will probably cause additional problems and errors in the code. So, I found another solution - it's related to instructing the code compiler not to use certain names during the mangling process. In other words, to set up a directive that prevents certain names from being used as shortened versions of normal variables.</p><p>I compile my code with <strong><a href="https://vitejs.dev/config/">Vite</a></strong>, so my solution is related to adjust build settings in <strong>vite.config.js</strong>.</p><p>By default, Vite uses <strong><a href="https://esbuild.github.io/">Esbuild </a></strong>web bundler for minifing code. I didn&#8217;t find easy options in Esbuild to fix my issue, so I switched from Esbuild to <strong><a href="https://terser.org/">Terser</a></strong> - beautiful javascript mangler and compressor. </p><p>First of all, we need to install terser in our project:</p><pre><code>npm install terser</code></pre><p>My conflicting variable is called &#8216;dc&#8217;. So we need to configure directive in build part to not use this variable in naming shorten variables in classes during compilation.</p><p>So let&#8217;s modify our vite.config.js.</p><pre><code>import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  resolve: {
   // some other code
  },
  // Build configuration
  build: {
    minify: 'terser', // here we set up terser instead default esbuild
    terserOptions: { 
     mangle: {
      reserved: ['dc'] // here we set up variable we want to exclude //from naming
     }
    },
    rollupOptions: {
      // some other code
    },
  },
});
</code></pre><p>Let's break it down:</p><ol><li><p><code>build:</code> This section of the configuration is specific to the build process of Vite project. The settings within this block are applied when you build your project for production.</p></li><li><p><code>minify: 'terser',</code> This line tells Vite to use the <code>terser</code> plugin for minification. </p></li><li><p><code>terserOptions:</code> This part allows to specify custom options that will be passed to <code>terser</code> during the minification process </p></li><li><p><code>mangle:</code> This is a specific option within <code>terser</code>. </p></li><li><p><code>reserved: ['dc']</code> Under the <code>mangle</code> option, this array <code>reserved</code> lists identifiers that should not be mangled. In this case, it's specifying that <code>terser</code> should not rename any variables or functions named <code>dc</code> during the minification process. </p></li></ol><p>So after changing my vite.config.js, I&#8217;ve re-built the script and not found any more &#8216;dc&#8217; variable there. </p><h3>Conclusion</h3><p>My example is related to Vite + Terser. However, it's just an example, so if you are using other tools to build your applications, libraries, and scripts, you may employ a similar strategy to handle such conflicts.</p><p>By instructing the compiler not to use specific names during the mangling process, you effectively prevent it from creating name clashes with other variables or libraries. This method is much safer and more reliable than manually renaming variables in the already minified code, which could indeed lead to further errors and issues. Your solution shows an understanding of how to handle potential conflicts in a controlled and systematic way.</p><p>Thanks for you attention and happy coding!</p><p>P.S. Subscribe for more interesting and useful posts about web development (especially if you are curious in Laravel and Vue.js). </p><p></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Labro:Dev! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Turn Vue.js 3.0 SPA to embedded widget]]></title><description><![CDATA[Seamlessly integrating Vue.js 3 SPA as Embedded Widget: A Step-by-Step Guide]]></description><link>https://labrodev.substack.com/p/turn-vuejs-30-spa-to-embedded-widget</link><guid isPermaLink="false">https://labrodev.substack.com/p/turn-vuejs-30-spa-to-embedded-widget</guid><dc:creator><![CDATA[Petro Lashyn]]></dc:creator><pubDate>Sun, 10 Dec 2023 16:33:06 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/e167de53-8c20-44b0-a736-ee37047346de_1320x1000.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Introduction</h3><p>I've encountered situations where a Single Page Application needs to be utilized as an embedded widget for integration into external websites.</p><p>Let's assume that, as a default requirement, this widget should be versatile enough to be seamlessly integrated into any website, without imposing any specific conditions. This task presents several challenges. On one hand, the embedded widget must remain isolated and self-contained to achieve the desired visual aesthetics and workflow. On the other hand, it must ensure that it does not interfere with the styling of the parent site or disrupt any of the external site's functions.</p><p>In this article, I aim to provide a comprehensive, step-by-step guide on transforming a Single Page Application constructed with<strong> Vue.js 3</strong> into a fully functional embedded widget.</p><p>In this particular context, an embedded widget refers to a compiled .js script that is incorporated into an external web page. It is specifically designated with a &lt;custom tag&gt; that serves as the entry point in the Document Object Model (DOM) for the widget elements, and this is where the SPA content will be rendered.</p><p>Let's jump into this concept with a practical example. Our objective is to create a countdown widget for tracking the time remaining until a specific date or event, for example start of the New Year or the end of discount campaigns or Black Friday time remaining counter. To implement this on an external website, it might appear as follows:</p><pre><code>&lt;html&gt; 

&lt;head&gt; 

&lt;script src="https://customdomain.com/countdown-widget.js"&gt;&lt;/script&gt; 

&lt;/head&gt; 

&lt;body&gt; 

&lt;countdown-widget date="2024-01-01 00:00:00" title="Time until the New Year:"&gt;&lt;/countdown-widget&gt; 

&lt;/body&gt;
&lt;/html&gt;</code></pre><p>Let&#8217;s assume that we could use some attributes to put from the external part into the widget so we could use them in our internal logic behind the widget blackbox.</p><p>Important thing is that the content of our embedded widget will be covered by the <strong>Shadow Root</strong>. Consequently, all styles will be encapsulated inside the widget and isolated from the external site. This is a very secure way to keep the widget stable on one hand and ensure that it will not affect the external site's styles.</p><p>The Shadow Root is a crucial aspect of web development, providing a means to encapsulate and isolate the styles, structure, and functionality of a web component from the rest of the document. It acts as a container for a component's content, creating a shadow DOM subtree that shields the component's internals from external styles and scripts. By utilizing Shadow Root, developers can prevent unintended interference with or from the host document, fostering modularity and maintaining a clear separation between different components on a webpage. This encapsulation is particularly valuable for creating custom elements and widgets, offering a secure and well-defined environment for their implementation and interaction within a broader web context.</p><p>Our technology stack for this project includes:</p><ul><li><p><strong><a href="https://vuejs.org/">Vue.js 3</a></strong></p></li><li><p><strong><a href="https://tailwindcss.com/">Tailwind CSS</a></strong></p></li><li><p><strong><a href="https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot?retiredLocale=uk">Shadow Root</a></strong></p></li><li><p><strong><a href="https://vitejs.dev/">Vite </a></strong></p></li><li><p><strong><a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements">Custom Element API</a></strong></p></li></ul><div><hr></div><h3>Step-by-step guide from scratch </h3><div><hr></div><h4>Vue.js 3 installation by Vite.</h4><p>Let&#8217;s create initial Vue.js 3 application by Vite.</p><p><a href="https://vitejs.dev/">Vite</a> is a build tool for modern web development that focuses on providing a fast development server with instant server start and optimized build times.</p><p>To get started with Vue.js 3, we'll need to follow a few steps. Before we begin, make sure we have Node.js and npm (Node Package Manager) installed on our system. It can be downloaded  from the official website: <a href="https://nodejs.org/">Node.js</a>.</p><p>Once we have Node.js and npm installed, we can proceed with creating a Vue.js 3 application.</p><h5>Step 1: Install Vite</h5><p>Let&#8217;s open terminal or command prompt and run the following command to install the Vue CLI globally:</p><pre><code>npm install vite</code></pre><h5><strong>Step 2: Create Vite</strong></h5><p>Let&#8217;s create a Vite instance using the following command:</p><pre><code>npm install -g create-vite</code></pre><p>In promt let&#8217;s choose project folder, Vue as framework and Javascript as language variant. </p><pre><code>&#10004; Project name: &#8230; vuejs-embedded-widget</code></pre><pre><code>&#10004; Select a framework: &#8250; Vue</code></pre><pre><code>&#10004; Select a variant: &#8250; JavaScript</code></pre><h5><strong>Step 3: Navigate to the Project Directory</strong></h5><p>Now let&#8217;s change our working directory to the newly created project:</p><pre><code>cd vuejs-embedded-widget</code></pre><h5><strong>Step 4: Run npm install</strong></h5><p>Let&#8217;s make initial of project packages from package.json:</p><pre><code>npm install</code></pre><h5><strong>Step 5: Run application to test the installation</strong></h5><pre><code>npm run dev</code></pre><p>We may see in promt something like that:</p><pre><code>
  VITE v5.0.7  ready in 331 ms

  &#10140;  Local:   http://localhost:5173/
  &#10140;  Network: use --host to expose
  &#10140;  press h + enter to show help
</code></pre><p>After running the development server,  we will be able to check our initial installation of Vue.js 3 at http://localhost:5173 (or any other available port provided by the command).</p><div><hr></div><h4>Configuring Tailwind CSS in Vue.js 3</h4><p>To install Tailwind CSS in our project, we will follow these steps. We already have a project set up, so we can integrate Tailwind CSS as follows:</p><h5><strong>Step 1: Install Tailwind CSS and its Dependencies</strong></h5><pre><code>npm install -D tailwindcss postcss autoprefixer</code></pre><h5><strong>Step 2: Create Tailwind Configuration Files</strong></h5><pre><code>npx tailwindcss init -p</code></pre><p>We may see in promt: </p><pre><code>Created Tailwind CSS config file: tailwind.config.js
Created PostCSS config file: postcss.config.js</code></pre><p>So as a result of this command will be created 2 important files in root of our project:</p><ul><li><p>Tailwind CSS config file: tailwind.config.js</p></li><li><p>PostCSS config file: postcss.config.js</p></li></ul><p><strong>tailwindcss.config.js</strong><code> </code>we will configure in further chapters as it eventually turns to be important for customization to solve the issue with responsive design for our embedded widget.</p><p><strong>postcss.config.js  </strong>we will update in next step.</p><h5>Step 3: Configure tailwind.config.js</h5><p>Let&#8217;s adjust tailwind.config.js:</p><pre><code>/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}</code></pre><h5><strong>Step 4: Configure </strong><code>postcss.config.js</code></h5><p>Let&#8217;s open the <strong>postcss.config.js</strong> file and configure it to use Tailwind CSS and autoprefixer. Let&#8217;s update the file to look like this:</p><pre><code>module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}</code></pre><h5><strong>Step 4: Create CSS File</strong></h5><p>Now we need to create a new CSS file, for example, <strong>src/assets/styles/main.css</strong>, and import Tailwind CSS styles:</p><pre><code><code>/* src/assets/styles/main.css */

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
</code></code></pre><h5><strong>Step 5: Import CSS </strong></h5><p>Let&#8217;s import the CSS file we just created into our main application file - <strong>src/main.js</strong>: </p><pre><code>// src/main.js

import Vue from 'vue';
import App from './App.vue';
import './assets/styles/main.css';

Vue.config.productionTip = false;

new Vue({
  render: h =&gt; h(App),
}).$mount('#app');</code></pre><p>That's it! We've successfully installed Tailwind CSS in our project. </p><p>By the way, if you are new to Tailwind CSS, you can find more detailed information about this framework, including use cases, implementation, and installation, in the <a href="https://tailwindcss.com/">official Tailwind CSS documentation</a> for additional details and customization options.</p><div><hr></div><h4>Develop our Vue.js 3 Single Page Application</h4><p>Let's now develop our functionality (Countdown till a given date/time) before jumping into the main topic of this article: transforming a simple Vue.js 3 app into an embedded widget script. </p><h5>Step 1: Create a component for Countdown </h5><p>Let&#8217;s create a component src/components/Countdown.vue and put there a logic. </p><pre><code>&lt;template&gt;
    &lt;div class="text-center mt-2" :style="{ color: textColor }"&gt;
        
        &lt;div v-if="countdownDateIsInvalid === true"&gt;
            :date property is invalid. Should be dateString in correct format.
        &lt;/div&gt;

        &lt;!-- Display a message when the countdown has expired --&gt;
        &lt;div v-if="countdownExpired &amp;&amp; end !== null" class="text-2xl"&gt;
            &lt;p&gt;{{ end }}&lt;/p&gt;
        &lt;/div&gt;
        &lt;!-- Display the countdown when it's still active --&gt;
        &lt;div v-else&gt;
            &lt;p class="text-4xl font-bold"&gt;
                {{ title }}
            &lt;/p&gt;
            &lt;!-- Display the countdown in days, hours, minutes, and seconds --&gt;
            &lt;p class="text-3xl font-bold mt-2"&gt;
                &lt;span&gt;{{ countdown.days }} days &lt;/span&gt;
                &lt;span&gt;{{ countdown.hours }} hours &lt;/span&gt;
                &lt;span&gt;{{ countdown.minutes }} minutes &lt;/span&gt;
                &lt;span&gt;{{ countdown.seconds }} seconds &lt;/span&gt;
            &lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/template&gt;

&lt;script setup&gt;
    // Import required functions from Vue
    import { ref, defineProps } from 'vue';

    // Define props for the component
    const props = defineProps({
        date: {
            type: String,
            required: true
        },
        title: {
            type: String,
            required: true
        },
        end: {
            type: String,
            required: true
        },
        color: {
            type: String,
            required: true
        }
    });

    // Calculate the timestamp for the countdown end date
    const countdownDate = ref(new Date(props.date).getTime());

    const countdownDateIsInvalid = isNaN(countdownDate.value);

    // Initialize the countdown values
    const countdown = ref({
        days: 0,
        hours: 0,
        minutes: 0,
        seconds: 0
    });

    // Track whether the countdown has expired
    const countdownExpired = ref(false);

    // Determine the text color based on the provided prop or default to black
    const textColor = props.color || 'black';

    // Update the countdown values every second
    // setInterval schedules repeated execution every delayed value (1000 ms = 1 second)
    setInterval(() =&gt; {
        // Get the current timestamp
        const now = new Date().getTime();

        // Calculate the remaining time in milliseconds
        const distance = countdownDate.value - now;

        // Check if the countdown has expired
        if (distance &lt;= 0) {
            countdownExpired.value = true;
            return;
        }

        // Calculate days, hours, minutes, and seconds
        countdown.value.days = Math.floor(distance / (1000 * 60 * 60 * 24));
        countdown.value.hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
        countdown.value.minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
        countdown.value.seconds = Math.floor((distance % (1000 * 60)) / 1000);
    }, 1000);

&lt;/script&gt;</code></pre><h5>Step 2: Add component to App.vue</h5><p>Let&#8217;s add component to App.vue with necessary properties needed to our Countdown widget.</p><pre><code>&lt;template&gt;
  &lt;CountDown 
      :date="props.date"
      :title="props.title"
      :end="props.end" 
      :color="props.color" /&gt;
&lt;/template&gt;

&lt;script setup&gt;
  import CountDown from "@/components/CountDown.vue";

  // Import required functions from Vue
  import {  defineProps } from 'vue';

  // Define props for the component
  const props = defineProps({
      date: {
          type: String,
          required: true
      },
      title: {
          type: String,
          required: true
      },
      end: {
          type: String,
          required: false,
          defaut: 'Countdown is end.'
      },
      color: {
          type: String,
          required: false,
          default: '#FF0000'
      }
  });
&lt;/script&gt;
</code></pre><p>Now, our SPA is ready and provided countdown logic. </p><h5><strong>Step 3: Run the application to check results</strong></h5><p>Let&#8217;s run application to check results and test our countdown logic. Run in console a command to serve application in development mode:</p><pre><code>npm run serve</code></pre><p>If you give development host in browser, it will look like: </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!piKN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!piKN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 424w, https://substackcdn.com/image/fetch/$s_!piKN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 848w, https://substackcdn.com/image/fetch/$s_!piKN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 1272w, https://substackcdn.com/image/fetch/$s_!piKN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!piKN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png" width="512" height="292.4088888888889" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:514,&quot;width&quot;:900,&quot;resizeWidth&quot;:512,&quot;bytes&quot;:50699,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!piKN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 424w, https://substackcdn.com/image/fetch/$s_!piKN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 848w, https://substackcdn.com/image/fetch/$s_!piKN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 1272w, https://substackcdn.com/image/fetch/$s_!piKN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa8153095-ceb4-4ed0-92ba-3c4d0d49e820_900x514.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h4><strong>Turn SPA to Embedded Widget</strong></h4><p>Here we are in the main part of this topic. Now let&#8217;s convert an SPA to an embedded widget so that we have a compiled .js script ready to be implemented on external sites.</p><p>The significant magic point here is to use <strong>Custom Elements API</strong>.</p><p>The Custom Elements API is a part of the web platform and is related to JavaScript, HTML, and web development in general. It is a standard web API that is not specific to any particular JavaScript framework or library, including Vue.js. </p><p>The Custom Elements API is a browser feature that allows developers to define and use their own custom HTML elements with encapsulated behavior. It enables you to create reusable components and extend the set of available HTML elements, making it easier to build modular and maintainable web applications.</p><p>Let&#8217;s create a <strong>bootstrap.js</strong> file in our <strong>/src</strong> folder. In this file, there will be logic to mount our app into the expected DOM element (which is the target argument). This element will be from a widget tag on the external site (&lt;countdown-widget&gt; in our case).</p><h5><strong>Step 1: Create bootstrap.js</strong></h5><pre><code>import { createApp } from 'vue';
import App from './App.vue';

// Function to bootstrap and mount the Vue.js application
export function bootstrap(target, attributes) {

    // Create a new Vue application instance
    // second argument is object of attributes that will be 
    // converted to properites in App.vue
    const app = createApp(App, attributes)
    
     // Mount the Vue application onto the specified target element
    app.mount(target);
}</code></pre><h5><strong>Step 2: Make custom element in main.js </strong></h5><p>Let&#8217;s then use our bootstrap function in main.js and use Custom Element API to turn to embedded widget. </p><pre><code>// Import the bootstrap function from the 'bootstrap.js' file
import { bootstrap } from './bootstrap.js';

// Define a custom element 'countdown-widget' using the Custom Elements API
customElements.define('countdown-widget', class extends HTMLElement {
 
  async connectedCallback() {

    // Create a shadow DOM for encapsulatio
    const shadowRoot = this.attachShadow({ mode: 'open' });

    // Fetch &lt;countdown-widget&gt; attributes 
    // to pass as properties for App.vue

    const attributes = {
      'date': this.getAttribute('date'),
      'title': this.getAttribute('title'),
      'end': this.getAttribute('end'),
      'color': this.getAttribute('color')
    }
    
    // Bootstrap the Vue.js application within the shadow DOM
    bootstrap(shadowRoot, attributes);
  }
});</code></pre><p><strong>Step 3: Configure vite.config.js</strong></p><p>Let&#8217;s configure vite.config.js to compile necessary files: .js file for script to include in external site and .css file to apply styles inside our application to be used in Shadow Root. </p><pre><code>import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [
    vue(),
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  build: {
    rollupOptions: {
      input: {
        widget: fileURLToPath(new URL('./src/main.js', import.meta.url)),
        style: './src/assets/styles/main.css'
      },
      output: {
        inlineDynamicImports: false,
        entryFileNames: '[name].js',      
        chunkFileNames: '[name].js',      
        assetFileNames: '[name].[ext]'
    },
  }
})</code></pre><h5>Step 4: Adjust package.json </h5><p>Let&#8217;s check package.json file. Check if there is a property:</p><pre><code>  "type": "module",</code></pre><p>If yes, then let&#8217;s change it to: </p><pre><code>  "type": "commonjs",</code></pre><h5>Step 5: Compile widget files </h5><p>Let&#8217;s compile widget files (.js and .css) which will be outputed in /dist folder. </p><pre><code>npx vite dist</code></pre><p>In folder /dist we may find 2 files:</p><ul><li><p>widget.js </p></li><li><p>style.css</p></li></ul><p>In promt we may see:</p><pre><code>
  VITE v5.0.7  ready in 209 ms

  &#10140;  Local:   http://localhost:5173/
  &#10140;  Network: use --host to expose
  &#10140;  press h + enter to show help</code></pre><p>If we go to <a href="http://localhost:5173/widget.js">http://localhost:5173/widget.js</a>, we can see the compiled, minified JavaScript code.</p><p>But let's transition from development mode to a configured host and production build so that we can thoroughly test the entire solution and deploy it on an external site. Consequently, we require a stable production build for our widget, ensuring that <strong>widget.js</strong> is accessible through a dedicated host.</p><h5>Step 6: Configure virtual host  </h5><p>Let&#8217;s configure virtual host (in this example, it will be Nginx). </p><p><em><a href="https://www.digitalocean.com/community/tutorials/how-to-install-nginx-on-ubuntu-20-04">Find more here how to deal with it if you are new in Nginx.</a></em></p><p>This file will be somewhere in /etc/nginx/sites-available:</p><pre><code>                                                                                        server {
     listen 80;

     root /var/www/vuejs-embedded-widget/dist;

     server_name vuejs-embedded-widget;

     location / {
             try_files $uri $uri/ /index.html?$query_string;
     }
}</code></pre><p>We may also need to add the "<strong>vue-js-embedded-widget</strong>" host to our local hosts list (while we are on a local server). You can find it in /etc/hosts. Add the following entry:</p><pre><code>127.0.0.1       vuejs-embedded-widget</code></pre><p>We also need to make symlink from /etc/nginx/sites-available/vuejs-embedded-widget.conf to /etc/nginx/sites-enabled. </p><pre><code>sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/</code></pre><p>Let&#8217;s restart nginx server:</p><pre><code>sudo service nginx restart</code></pre><h5><strong>Step 7: Make build</strong> </h5><p>Let&#8217;s make a build to  compile widget.js and style.css and to have them available by our virtual host: </p><pre><code>npx vite build </code></pre><p>Now we are able to reach compiled widget files: </p><ul><li><p>http://vuejs-embedded-widget/widget.js </p></li><li><p>http://vuejs-embedded-widget/style.css</p><p></p></li></ul><h5>Step 8: Link compiled style.css to Shadow Root</h5><p>Let's include our compiled s<strong>tyle.css </strong>directly into the <strong>App.vue </strong>template using the &lt;link&gt; attribute. This implies that our App.vue will be within the Shadow Root, resulting in the inclusion of style.css inside the Shadow Root. </p><p>Consequently, CSS styles from style.css will be applied to the widget's DOM content.</p><pre><code>&lt;template&gt;
  &lt;!--Link to compiled style.css--&gt;
  &lt;link rel="stylesheet" href="http://vuejs-embedded-widget/style.css"&gt;

  &lt;CountDown 
      :date="props.date"
      :title="props.title"
      :end="props.end" 
      :color="props.color" /&gt;
&lt;/template&gt;

&lt;script setup&gt;
  import CountDown from "@/components/CountDown.vue";

  // Import required functions from Vue
  import {  defineProps } from 'vue';

  // Define props for the component
  const props = defineProps({
      date: {
          type: String,
          required: true
      },
      title: {
          type: String,
          required: true
      },
      end: {
          type: String,
          required: false,
          defaut: 'Countdown is end.'
      },
      color: {
          type: String,
          required: false,
          default: '#FF0000'
      }
  });
&lt;/script&gt;</code></pre><h5>Step 9: Re-compile files with new build to apply changes in App.vue</h5><pre><code>npx vite build</code></pre><p>That&#8217;s all! </p><p>Now that we have <strong>widget.js</strong>, we can implement our Countdown widget to render on any website without dependencies on the website's tech stack.</p><div><hr></div><h4>Implement widget in external site </h4><p>Let's assume that we are still on a local environment, so we will use <strong>http://vuejs-embedded-widget</strong> (as configured in the previous step). And, of course, the site where it will be implemented should also be on a local server in this case.</p><h5>Step 1: Add widget.js to site &lt;head&gt;</h5><pre><code>&lt;head&gt;

&lt;script src="http://vuejs-embedded-widget/widget.js" async&gt;&lt;/script&gt;

&lt;/head&gt;</code></pre><h5>Step 2: Put widget tag inside &lt;body&gt;&lt;/body&gt; </h5><p>Let&#8217;s put &lt;countdown-widget&gt;&lt;/countdown-widget&gt; with necessary attributes in external site body in place where it should be rendered:</p><pre><code>&lt;countdown-widget 
   date="2024-06-14" 
   title="Euro 2024 will start in"
   end="Euro 2024 is started!"
   color="#FF0000"&gt;
&lt;/countdown-widget&gt;</code></pre><p>And now it works! </p><p>We may find inside DOM of the external site where the widget is embedded how it&#8217;s rendered with <strong>#shadow-root</strong> block: </p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zD0Z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zD0Z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 424w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 848w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 1272w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zD0Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png" width="900" height="100" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:100,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:51778,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zD0Z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 424w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 848w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 1272w, https://substackcdn.com/image/fetch/$s_!zD0Z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8ae2bce-3aa5-4238-b2b4-804ec44fcd80_900x100.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div><hr></div><h3>Conclusion</h3><p>This tutorial describes the concept of using Vue.js 3 SPA as an embedded widget. It provides a simple example of Countdown functionality, but it can also be applied to more complex Vue.js applications involving APIs, Axios, Store management, etc.</p><p>An important aspect is the utilization of the Shadow Root, which serves as an effective means to deliver embedded components for use in external sites without the need for an &lt;iframe&gt;.</p><p>I will continue this topic in the next article and use this project as a base to cover some other issues related to building embedded widgets with Vue.js. For example, I'll show how to make it mobile-friendly, as we need to define the size on the parent block where the widget is embedded, not based on the size of the screen. Additionally, I will explain how to adjust the Tailwind theme configuration to correctly reflect the size definition. </p><p>You may find repository with code covered in this article on <a href="https://github.com/labrodev/vuejs-embedded-widget">Labro Dev Github. </a></p><div><hr></div><p>Thanks for reading this article, and I hope it was helpful for you. Please subscribe to the Labro Dev Substack, where our Labro Dev team will continue to share our experiences related to web development, with a focus on Laravel and Vue.js.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://labrodev.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://labrodev.substack.com/subscribe?"><span>Subscribe now</span></a></p>]]></content:encoded></item></channel></rss>