<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Chris Shennan</title>
    <description>The latest articles on Forem by Chris Shennan (@chrisshennan).</description>
    <link>https://forem.com/chrisshennan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F638038%2F7097dcd5-4ceb-42e2-a93b-1aa63e122dcf.png</url>
      <title>Forem: Chris Shennan</title>
      <link>https://forem.com/chrisshennan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/chrisshennan"/>
    <language>en</language>
    <item>
      <title>The Changelog - February 2026</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Wed, 11 Mar 2026 12:04:11 +0000</pubDate>
      <link>https://forem.com/chrisshennan/the-changelog-february-2026-1gh6</link>
      <guid>https://forem.com/chrisshennan/the-changelog-february-2026-1gh6</guid>
      <description>&lt;p&gt;&lt;strong&gt;February in a nutshell&lt;/strong&gt;: Streamlining Scaffold’s asset management, trying new marketing workflows, and keeping the momentum going, one small improvement at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Statistics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Revenue
&lt;/h3&gt;

&lt;p&gt;Goal: &lt;a href="https://chrisshennan.com/blog/new-years-resolution-2026-1000-mmr" rel="noopener noreferrer"&gt;£1,000 MMR&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Jan&lt;/th&gt;
&lt;th&gt;Feb&lt;/th&gt;
&lt;th&gt;Mar (11th)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;BuyMeACoffee.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£0&lt;/td&gt;
&lt;td&gt;£0&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/sponsors/chrisshennan" rel="noopener noreferrer"&gt;GitHub Sponsors&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;£0&lt;/td&gt;
&lt;td&gt;£0&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Followers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Jan&lt;/th&gt;
&lt;th&gt;Feb&lt;/th&gt;
&lt;th&gt;Mar (11th)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://bsky.app/profile/chrisshennan.bsky.social" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://x.com/chrisshennan" rel="noopener noreferrer"&gt;X&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://linkedin.com/in/chrisshennan" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;759&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://newsletter.chrisshennan.com/" rel="noopener noreferrer"&gt;Newsletter&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Am I missing something? What other statistics would you like to see?&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Feature - Scaffold Asset Management
&lt;/h2&gt;

&lt;p&gt;February saw me continuing to improve the Scaffold Asset Management, simplifying the structure of the &lt;a href="https://github.com/chrisshennan/scaffold/blob/main/bundles/Scaffold/CoreBundle/manifest/manifest.json" rel="noopener noreferrer"&gt;manifest.json&lt;/a&gt; file&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grouping files by a section type&lt;/li&gt;
&lt;li&gt;reducing verbose configuration via sensible defaults&lt;/li&gt;
&lt;li&gt;adding files I missed, like the .releaserc.json (configuration to allow semantic versioning), .editorconfig (PHPStorm editor settings)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I then used the updated Scaffold Asset Management to bring &lt;a href="https://buildwithscaffold.com" rel="noopener noreferrer"&gt;Build With Scaffold&lt;/a&gt; up to date with the latest version. &lt;/p&gt;

&lt;h2&gt;
  
  
  Marketing - &lt;a href="https://SkySurge.app" rel="noopener noreferrer"&gt;SkySurge&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;After I mentioned &lt;a href="https://chrisshennan.com/blog/the-changelog-january-2026" rel="noopener noreferrer"&gt;in my last post&lt;/a&gt; that I had started using Buffer to schedule some of my &lt;a href="https://bsky.app/profile/chrisshennan.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; and &lt;a href="https://x.com/chrisshennan" rel="noopener noreferrer"&gt;X&lt;/a&gt; posts, Henrik reached out and suggested that I give &lt;a href="https://SkySurge.app" rel="noopener noreferrer"&gt;SkySurge&lt;/a&gt; a go.  He even &lt;a href="https://bsky.app/profile/skysurge.app/post/3mf7mkz4hm22l" rel="noopener noreferrer"&gt;admitted it was a shameless plug&lt;/a&gt;, as it was for his own service, but the timing worked as it aligned with my objective - to increase my follower count.&lt;/p&gt;

&lt;p&gt;While using SkySurge, I sent Henrik several suggestions for improvements, some UX niggles and a few ideas for new features, which Henrik graciously took on board, and had some of them done and deployed within a few hours.  Even my feedback on making the application responsive on mobile was turned around in just a couple of days.&lt;/p&gt;

&lt;p&gt;I've not done much user testing in the past, so this was a nice change of pace, and a win-win - Henrik got feedback on his product, and I got a tool to help me try and get involved in more conversations.&lt;/p&gt;

&lt;p&gt;I've added a new "The Statistics" section to the top of this post, and I'll add it to my future posts so you can track my progress.&lt;/p&gt;

&lt;p&gt;If you’d like to help me with that and follow along as I build in public, you can find me on &lt;a href="https://bsky.app/profile/chrisshennan.bsky.social" rel="noopener noreferrer"&gt;Bluesky&lt;/a&gt; or &lt;a href="https://x.com/chrisshennan" rel="noopener noreferrer"&gt;X&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blog Post
&lt;/h2&gt;

&lt;p&gt;Things were quite quiet on the blogging front.  I did hit an issue with the &lt;code&gt;pdo_mysql&lt;/code&gt; PHP extensions intermittently disappearing with my latest Scaffold project and I wrote up the &lt;a href="https://chrisshennan.com/blog/intermittent-pdomysql-php-extension-in-docker-diagnosis-and-fix" rel="noopener noreferrer"&gt;diagnosis and fix&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No progress this month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Anything else?
&lt;/h2&gt;

&lt;p&gt;When mentioning &lt;a href="https://bsky.app/profile/chrisshennan.bsky.social/post/3mf5xf7to4x25" rel="noopener noreferrer"&gt;how disorganised I was when trying to pull together my notes for The Changelog - January 2026&lt;/a&gt;, the discussion turned to what I'm using to track my progress.  This is primarily via Evernote, but I did highlight that I was having issues with it due to it converting Markdown to rich text format automatically, and a few Evernote alteratives were suggested.&lt;/p&gt;

&lt;p&gt;I tried a few, and landed on &lt;a href="https://flatnotes.io" rel="noopener noreferrer"&gt;FlatNotes&lt;/a&gt;.  First impressions were really good:-&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's easy to set up - just a simple docker command or docker compose file&lt;/li&gt;
&lt;li&gt;It allows me to work with raw Markdown files easily&lt;/li&gt;
&lt;li&gt;It has a web interface - this is a core requirement for me with FlatNotes being self-hosted as I need to be able to use if from multiple devices (laptop / phone)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I initially tried it on my local machine, and it worked great so I tried to move it to a dedicated setup using an old Raspberry Pi, but the Raspberry Pi was too old and could only run the 32-bit OS and FlatNotes only has 64-bit docker images, so I'm back to using Evernote for now.&lt;/p&gt;

&lt;p&gt;My next attempt will be to set it up on a Digital Ocean droplet, and the cost should be pretty much the same (Digital Ocean droplet cost vs Evernote Starter subscription), but my current Evernote subscription is valid for another 6 months so I've got a bit of time to swap over.  The biggest factor for how quickly I'll move over to &lt;a href="https://flatnotes.io" rel="noopener noreferrer"&gt;FlatNotes&lt;/a&gt; will be how much frustration I encounter while still trying to use Evernote.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://chrisshennan.com/blog/the-changelog-february-2026" rel="noopener noreferrer"&gt;https://chrisshennan.com/blog/the-changelog-february-2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>sideprojects</category>
      <category>indiehackers</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fixing Docker‑Compose, PhpStorm &amp; Xdebug: Lesson from a Port &amp; Socket Mix‑up</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Thu, 24 Jul 2025 13:07:00 +0000</pubDate>
      <link>https://forem.com/chrisshennan/fixing-docker-compose-phpstorm-xdebug-lesson-from-a-port-socket-mix-up-4f83</link>
      <guid>https://forem.com/chrisshennan/fixing-docker-compose-phpstorm-xdebug-lesson-from-a-port-socket-mix-up-4f83</guid>
      <description>&lt;p&gt;I recently hit a frustrating edge‑case with Docker, PhpStorm and Xdebug - and wanted to walk through what happened and how I fixed it, in case you end up in the same situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom: “ports are not available” on port 3306
&lt;/h2&gt;

&lt;p&gt;It started simple enough - Docker failed to start my MySQL container:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:3306 -&amp;gt; 127.0.0.1:0: listen tcp […] bind: address already in use&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;netstat &lt;span class="nt"&gt;-tulnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;3306
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tcp  0.0.0.0:3306  LISTEN  6885/docker-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant Docker itself was already proxying 3306 - likely due to another container still running. So I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ran &lt;code&gt;docker ps&lt;/code&gt; which shows no container running&lt;/li&gt;
&lt;li&gt;Ran &lt;code&gt;sudo docker ps&lt;/code&gt; which showed the MySQL container running on port 3306&lt;/li&gt;
&lt;li&gt;Stopped and removed it with &lt;code&gt;sudo docker compose down&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That immediately solved the port conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next: PhpStorm could no longer run PHPUnit tests via Docker
&lt;/h2&gt;

&lt;p&gt;I then ran, as my local user &lt;code&gt;chris&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And all the containers started as expected.  But now when I ran my tests in PhpStorm they failed with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Failed to start docker‑compose service, start it in a command line and try again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Yet everything worked fine from the CLI. What changed?&lt;/p&gt;

&lt;p&gt;Turns out the issue wasn’t ports - it was the Docker socket PhpStorm was using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The root cause: a user‑specific Docker socket&lt;/strong&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker context inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;revealed the CLI was talking to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unix:///home/ubuntu/.docker/desktop/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That meant Docker was running under my username (e.g. via Linux/WSL’s Docker Desktop), not the system socket. PhpStorm was still trying to connect to var/run/docker.sock, so none of the containers it saw were the ones I just started.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I fixed it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Point PhpStorm at the right socket
&lt;/h3&gt;

&lt;p&gt;In PhpStorm settings → Build, Execution, Deployment → Docker:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edited the Docker configuration.&lt;/li&gt;
&lt;li&gt;Changed the Unix socket to:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unix:///home/chris/.docker/desktop/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reconfigure PhpStorm’s interpreter &amp;amp; test runner
&lt;/h3&gt;

&lt;p&gt;Now with PhpStorm connected to the correct Docker instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Went to Settings → PHP → CLI Interpreters&lt;/li&gt;
&lt;li&gt;Removed any old Docker‑based interpreters tied to the wrong socket.&lt;/li&gt;
&lt;li&gt;Added a new one pointing at my service’s PHP container.&lt;/li&gt;
&lt;li&gt;Verified Settings → PHP → Test Frameworks recognized it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The major takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Port conflicts? Check which docker‑proxy is listening and remove the conflicting container or change the host port.&lt;/li&gt;
&lt;li&gt;PhpStorm not seeing running containers? Make sure it’s hooked to the same socket that Docker CLI is.&lt;/li&gt;
&lt;li&gt;Always start containers and IDEs under the same user and context.&lt;/li&gt;
&lt;li&gt;If you switch Docker setups (e.g. system vs Desktop), update PhpStorm’s Docker settings accordingly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps if you find yourself stuck a few steps removed from the real issue. Let me know if you’re still facing Xdebug or interpreter mapping problems - I’ve walked through quite a few of those myself lately!&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://chrisshennan.com/blog/fixing-docker-compose-phpstorm-and-xdebug-lesson-from-a-port-and-socket-mix-up" rel="noopener noreferrer"&gt;https://chrisshennan.com/blog/fixing-docker-compose-phpstorm-and-xdebug-lesson-from-a-port-and-socket-mix-up&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>php</category>
      <category>phpstorm</category>
      <category>xdebug</category>
    </item>
    <item>
      <title>Decorate the Symfony router to add a trailing slash to all URLs</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Fri, 05 Jul 2024 13:21:43 +0000</pubDate>
      <link>https://forem.com/chrisshennan/decorate-the-symfony-router-to-add-a-trailing-slash-to-all-urls-40jd</link>
      <guid>https://forem.com/chrisshennan/decorate-the-symfony-router-to-add-a-trailing-slash-to-all-urls-40jd</guid>
      <description>&lt;p&gt;I recently noticed an issue between the links that Symfony generated for &lt;a href="https://passwordangel.co" rel="noopener noreferrer"&gt;Password Angel&lt;/a&gt; and the actual links that are in use.  When Symfony builds the URL there are no trailing slashes i.e. &lt;code&gt;/terms&lt;/code&gt;, however, as &lt;a href="https://passwordangel.co" rel="noopener noreferrer"&gt;Password Angel&lt;/a&gt; is hosted in an S3 bucket as a static site a trailing slash is part of the live URL i.e. &lt;code&gt;/terms/&lt;/code&gt;.  This causes 2 problems:-&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unnecessary redirections - All links in the page will refer to the link version without the trailing slash and then the user will need to be redirected to the version with the trailing slash.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The canonical URLs are invalid - As I'm using Symfony to generate the canonical URL for each page, it generated the link version without the trailing slash.  This may cause SEO issues as search engines will &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visit &lt;code&gt;/terms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;be redirected to &lt;code&gt;/terms/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;be informed the original page is at &lt;code&gt;/terms&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;... go to step 1 - infinite loop ...&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution - Decorate the Symfony Router
&lt;/h2&gt;

&lt;p&gt;To resolve this I created a decorator for the Symfony default router and have overridden the &lt;code&gt;generate&lt;/code&gt; method to add a slash to the end of the URL.  It also checks for the presence of &lt;code&gt;?&lt;/code&gt; which would indicate there are query string parameters and in this situation, I am inserting the &lt;code&gt;/&lt;/code&gt; before the &lt;code&gt;?&lt;/code&gt; as we want &lt;code&gt;/terms/?utm_campaign=...&lt;/code&gt; and not &lt;code&gt;/terms?utm_campaign=.../&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\DependencyInjection\Attribute\AsDecorator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\RequestContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\RouteCollection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\Router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Routing\RouterInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[AsDecorator('router.default')]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TrailingSlashUrlGenerator&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RouterInterface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;WarmableInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;Router&lt;/span&gt; &lt;span class="nv"&gt;$urlGenerator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$referenceType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ABSOLUTE_PATH&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Original URL&lt;/span&gt;
        &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$referenceType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Add the slash before any query string parameters&lt;/span&gt;
        &lt;span class="nv"&gt;$pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'?'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pos&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str_ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$parts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Add the slash at the end of the URL&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str_ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

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

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$pathinfo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pathinfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getRouteCollection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;RouteCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getRouteCollection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;RequestContext&lt;/span&gt; &lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;RequestContext&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;urlGenerator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;warmUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$cacheDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$buildDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: To host &lt;a href="https://passwordangel.co" rel="noopener noreferrer"&gt;Password Angel&lt;/a&gt; as a static site on S3, I have written a Symfony command to generate static versions of all the pages (all 4 of them) and these are uploaded to S3.  Let me know if you're interested and I'll post up how the Symfony command works.&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://chrisshennan.com/blog/decorate-the-symfony-router-to-add-a-trailing-slash-to-all-urls" rel="noopener noreferrer"&gt;https://chrisshennan.com/blog/decorate-the-symfony-router-to-add-a-trailing-slash-to-all-urls&lt;/a&gt;&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>routing</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Using PHP Attributes to Create and Use a Custom Validator in Symfony</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Wed, 26 Jun 2024 14:00:00 +0000</pubDate>
      <link>https://forem.com/chrisshennan/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony-50e9</link>
      <guid>https://forem.com/chrisshennan/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony-50e9</guid>
      <description>&lt;p&gt;Symfony, a leading PHP framework, is consistently updated to leverage modern PHP features. With PHP 8, attributes provide a new way to define metadata for classes, methods, properties, etc., which can be used for validation constraints. This blog post will guide you through creating and using a custom validator in Symfony to validate UK mobile number prefixes using PHP attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are PHP Attributes?
&lt;/h2&gt;

&lt;p&gt;PHP attributes, introduced in PHP 8, enable you to add metadata to various code elements, accessible via reflection. In Symfony, attributes can simplify defining validation constraints, making your code more concise and readable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example 1 - Creating a Custom Validator for UK Mobile Number Prefix
&lt;/h2&gt;

&lt;p&gt;Let's create a custom validator to check if a phone number has a valid UK mobile prefix (e.g., starting with '07').&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Attribute
&lt;/h3&gt;

&lt;p&gt;Create a new attribute class that defines the custom constraint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Validator/Constraints/UkMobile.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Attribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UkMobile&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Constraint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'The number "{{ string }}" is not a valid UK mobile number.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Validator
&lt;/h3&gt;

&lt;p&gt;Next, create the validator with the logic to check if a phone number has a valid UK mobile prefix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Validator/Constraints/UkMobileValidator.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\ConstraintValidator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Exception\UnexpectedTypeException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UkMobileValidator&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ConstraintValidator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Constraint&lt;/span&gt; &lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnexpectedTypeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if the number starts with '07'&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/^07[0-9]{9}$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{{ string }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Apply the Attribute in an Entity
&lt;/h3&gt;

&lt;p&gt;Use the UkMobile attribute in your entities to enforce this custom validation rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Entity/User.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;AppAssert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\ORM\Mapping&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="no"&gt;ORM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[ORM\Entity]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\Id]&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\GeneratedValue]&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\Column(type: 'integer')]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="na"&gt;#[ORM\Column(type: 'string', length: 15)]&lt;/span&gt;
    &lt;span class="na"&gt;#[Assert\NotBlank]&lt;/span&gt;
    &lt;span class="na"&gt;#[AppAssert\UkMobile]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$mobileNumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// getters and setters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Test the Validator
&lt;/h3&gt;

&lt;p&gt;Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/Validator/Constraints/UkMobileValidatorTest.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Tests\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints\UkMobile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints\UkMobileValidator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Test\ConstraintValidatorTestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UkMobileValidatorTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ConstraintValidatorTestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createValidator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;ConstraintValidator&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UkMobileValidator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testNullIsValid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UkMobile&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testValidUkMobileNumber&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'07123456789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UkMobile&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testInvalidUkMobileNumber&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$constraint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UkMobile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'08123456789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{{ string }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'08123456789'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertRaised&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example 2 - Creating a Custom Validator for Glasgow Postcodes
&lt;/h2&gt;

&lt;p&gt;In this example, we want to create a custom validator to check if a postcode is a valid Glasgow postcode.  This could be used for professional trade services i.e. &lt;a href="https://bark.com" rel="noopener noreferrer"&gt;bark.com&lt;/a&gt; where a company only services certain areas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define the Attribute
&lt;/h3&gt;

&lt;p&gt;First, create a new attribute class to define the custom constraint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Validator/Constraints/GlasgowPostcode.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Attribute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_METHOD)]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcode&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Constraint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'The postcode "{{ string }}" is not a valid Glasgow postcode.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Create the Validator
&lt;/h3&gt;

&lt;p&gt;Next, create the validator with the logic to check if a postcode is a valid Glasgow postcode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Validator/Constraints/GlasgowPostcodeValidator.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\ConstraintValidator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Exception\UnexpectedTypeException&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcodeValidator&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ConstraintValidator&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Constraint&lt;/span&gt; &lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UnexpectedTypeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Regex for validating Glasgow postcodes (starting with G)&lt;/span&gt;
        &lt;span class="nv"&gt;$pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/^G\d{1,2}\s?\d[A-Z]{2}$/i'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{{ string }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Apply the Attribute in an Entity
&lt;/h3&gt;

&lt;p&gt;Use the GlasgowPostcode attribute in your entities to enforce this custom validation rule.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Entity/Address.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Entity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;AppAssert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Doctrine\ORM\Mapping&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="no"&gt;ORM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[ORM\Entity]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Address&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\Id]&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\GeneratedValue]&lt;/span&gt;
    &lt;span class="na"&gt;#[ORM\Column(type: 'integer')]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="na"&gt;#[ORM\Column(type: 'string', length: 10)]&lt;/span&gt;
    &lt;span class="na"&gt;#[Assert\NotBlank]&lt;/span&gt;
    &lt;span class="na"&gt;#[AppAssert\GlasgowPostcode]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nv"&gt;$postcode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// getters and setters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Test the Validator
&lt;/h3&gt;

&lt;p&gt;Ensure everything works correctly by writing some unit tests or using Symfony's built-in validation mechanism.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/Validator/Constraints/GlasgowPostcodeValidatorTest.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Tests\Validator\Constraints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints\GlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints\GlasgowPostcodeValidator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Test\ConstraintValidatorTestCase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcodeValidatorTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ConstraintValidatorTestCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;createValidator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;ConstraintValidator&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcodeValidator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testNullIsValid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testValidGlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'G1 1AA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertNoViolation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;testInvalidGlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$constraint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GlasgowPostcode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'EH1 1AA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;buildViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$constraint&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;setParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{{ string }}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'EH1 1AA'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertRaised&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Beyond Entities
&lt;/h2&gt;

&lt;p&gt;Custom validators aren't restricted to entities.  They can be used to apply validation to properties and methods of any class you need, for example, if we wanted to use the GlasgowPostcode validator in a DTO object we could do something like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/DTO/PostcodeDTO.php&lt;/span&gt;
&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\DTO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;AppAssert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Component\Validator\Constraints&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostcodeDTO&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Assert\NotBlank]&lt;/span&gt;
    &lt;span class="na"&gt;#[AppAssert\GlasgowPostcode]&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$postcode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$postcode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;postcode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$postcode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getPostcode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;postcode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and to check this DTO contains valid data we would make use of the validation service like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$postcodeDTO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostcodeDTO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'G1 1AA'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$postcodeDTO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Using PHP attributes to define custom validators in Symfony can enhance code readability and leverage modern PHP features. By following the steps outlined above, you can create robust, reusable validation logic that integrates seamlessly with Symfony's validation system. This approach simplifies adding custom validations and keeps your code clean and maintainable.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;




&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony" rel="noopener noreferrer"&gt;https://chrisshennan.com/blog/using-php-attributes-to-create-and-use-a-custom-validator-in-symfony&lt;/a&gt;&lt;/p&gt;

</description>
      <category>symfony</category>
      <category>php</category>
      <category>attributes</category>
      <category>validation</category>
    </item>
    <item>
      <title>Using Tailwind CSS with Symfony Encore</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Mon, 01 Aug 2022 08:45:42 +0000</pubDate>
      <link>https://forem.com/chrisshennan/using-tailwind-css-with-symfony-encore-3k22</link>
      <guid>https://forem.com/chrisshennan/using-tailwind-css-with-symfony-encore-3k22</guid>
      <description>&lt;h2&gt;
  
  
  Why Tailwind CSS?
&lt;/h2&gt;

&lt;p&gt;If you are reading this, you are probably in the same situation as me i.e. you can do backend development without breaking a sweat, but when you try your hand at some front-end development, it ends up looking like a throwback to 1994.   &lt;/p&gt;

&lt;p&gt;I've tried bootstrap and various themes, and whilst they help, they tend to focus on a handful of specific page templates and deviating from them leaves me in the same situation as before.&lt;/p&gt;

&lt;p&gt;Tailwind, on the other hand, has a collection of components that you can piece together, any way you want, to build up your own templates and spans everything from marketing pages to an e-commerce store and checkout pages to application UI, meaning you can get your product up and running with a beautiful looking front-end with little effort and leaving you more time to do the stuff you are good at.&lt;/p&gt;

&lt;p&gt;The downside is you need to pay for the components - currently £219 + VAT, but in my view, it is well worth it as it includes&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All the components to build your marketing, application or e-commerce sites&lt;/li&gt;
&lt;li&gt;Lifetime membership - free updates forever&lt;/li&gt;
&lt;li&gt;Unlimited projects - no need to purchase for each project&lt;/li&gt;
&lt;li&gt;Pre-made templates using Tailwind CSS ready to use - this is something they have just recently added.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've spent much more than that over the years on templates that I've never quite managed to get working, whilst my first attempt at a project using Tailwind (&lt;a href="https://passwordangel.co" rel="noopener noreferrer"&gt;https://passwordangel.co&lt;/a&gt;) had the front-end up and running inside an hour.&lt;/p&gt;

&lt;p&gt;You can see all the available components and templates on the Tailwind UI site - &lt;a href="https://tailwindui.com/" rel="noopener noreferrer"&gt;https://tailwindui.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So without further ado, let's get stuck into getting Tailwind CSS up and running in your Symfony project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Install Symfony
&lt;/h4&gt;

&lt;p&gt;In this example, I'm going to set up a webapp symfony project because I want all the typical components for a web application (twig, routing, etc)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;symfony new &lt;span class="nt"&gt;--webapp&lt;/span&gt; symfony-tailwind-demo
&lt;span class="nb"&gt;cd &lt;/span&gt;symfony-tailwind-demo
composer update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm also going to need the Symfony Webpack Encore bundle to provide the frontend integration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require symfony/webpack-encore-bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Install node modules
&lt;/h4&gt;

&lt;p&gt;These are the node modules that are required by the Encore bundle to provider the frontend functionality and the tailwind module to help build the CSS require for the templates that you build.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn add &lt;span class="nt"&gt;--dev&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    @hotwired/stimulus core-js &lt;span class="se"&gt;\&lt;/span&gt;
    @symfony/stimulus-bridge &lt;span class="se"&gt;\&lt;/span&gt;
    @symfony/webpack-encore &lt;span class="se"&gt;\&lt;/span&gt;
    autoprefixer &lt;span class="se"&gt;\&lt;/span&gt;
    postcss &lt;span class="se"&gt;\&lt;/span&gt;
    postcss-loader &lt;span class="se"&gt;\&lt;/span&gt;
    tailwindcss &lt;span class="se"&gt;\&lt;/span&gt;
    webpack-notifier@^1.6.0 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now initialise tailwind so it creates your &lt;code&gt;tailwind.config.js&lt;/code&gt; and &lt;code&gt;postcss.config.js&lt;/code&gt; files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn tailwindcss init &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Configuration
&lt;/h4&gt;

&lt;p&gt;We need to specify which files to watch for changes in the &lt;code&gt;/tailwind.config.js&lt;/code&gt; file.  In our case,  we want to watch for changes to any of the twig template files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./templates/**/*.twig&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to enable the post CSS loader in the &lt;code&gt;webpack.config.js&lt;/code&gt; so it can resolve the tailwind CSS directives (we will get to that in a minute) into the desired CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webpack.config.js&lt;/span&gt;
&lt;span class="nx"&gt;Encore&lt;/span&gt;
    &lt;span class="c1"&gt;// directory where compiled assets will be stored&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOutputPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public/build/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;// ... rest of the default configuration ...&lt;/span&gt;

    &lt;span class="c1"&gt;// Add this line to enable the post CSS loader&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enablePostCssLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Encore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getWebpackConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating our first page using Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;Now that we have got the setup done and out the way, we can start to build our first page.  Lets start by adding the tailwind CSS directives to our CSS file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* /assets/styles/app.css */&lt;/span&gt;

&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then adding the &lt;code&gt;encore_entry_link_tags&lt;/code&gt; and &lt;code&gt;encore_entry_script_tags&lt;/code&gt; twig functions to our base template so encore can load in the CSS and javascript&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="c"&gt;{# templates/base.html.twig #}&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;block&lt;/span&gt; &lt;span class="nv"&gt;stylesheets&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
            &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;encore_entry_link_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endblock&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;block&lt;/span&gt; &lt;span class="nv"&gt;javascripts&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
            &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;encore_entry_script_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endblock&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;block&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%}{%&lt;/span&gt; &lt;span class="k"&gt;endblock&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Next, we want to add a routing file so we've got an entry point into the application&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.yaml&lt;/span&gt;
&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;
    &lt;span class="na"&gt;controller&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;App\Controller\DefaultController::index&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;and then the corresponding controller&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;// src/Controller/DefaultController.php&lt;/span&gt;

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

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Symfony\Bundle\FrameworkBundle\Controller\AbstractController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DefaultController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AbstractController&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/**
     * @return \Symfony\Component\HttpFoundation\Response
     */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'default/index.html.twig'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now the view template.  Lets take a component from the preview section from &lt;a href="https://tailwindui.com/preview" rel="noopener noreferrer"&gt;https://tailwindui.com/preview&lt;/a&gt;.  In this case I'm going to use "Ecommerce -&amp;gt; Components -&amp;gt; Promo Sections -&amp;gt; With image tiles"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight twig"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="s1"&gt;'base.html.twig'&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;

&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;block&lt;/span&gt; &lt;span class="nv"&gt;body&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- This example requires Tailwind CSS v2.0+ --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative bg-white overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-16 pb-80 sm:pt-24 sm:pb-40 lg:pt-40 lg:pb-48"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 sm:static"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sm:max-w-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-4xl font font-extrabold tracking-tight text-gray-900 sm:text-6xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Summer styles are finally here&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-4 text-xl text-gray-500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This year, our new summer collection will shelter you from the harsh elements of a world that doesn't care if you live or die.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="c"&gt;&amp;lt;!-- Decorative image grid --&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pointer-events-none lg:absolute lg:inset-y-0 lg:max-w-7xl lg:mx-auto lg:w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"absolute transform sm:left-1/2 sm:top-0 sm:translate-x-8 lg:left-1/2 lg:top-1/2 lg:-translate-y-1/2 lg:translate-x-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center space-x-6 lg:space-x-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-shrink-0 grid grid-cols-1 gap-y-6 lg:gap-y-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden sm:opacity-0 lg:opacity-100"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-01.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-02.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-shrink-0 grid grid-cols-1 gap-y-6 lg:gap-y-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-03.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-04.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-05.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-shrink-0 grid grid-cols-1 gap-y-6 lg:gap-y-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-06.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-44 h-64 rounded-lg overflow-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                      &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tailwindui.com/img/ecommerce-images/home-page-03-hero-image-tile-07.jpg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full h-full object-center object-cover"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

            &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"inline-block text-center bg-indigo-600 border border-transparent rounded-md py-3 px-8 font-medium text-white hover:bg-indigo-700"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Shop Collection&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{%&lt;/span&gt; &lt;span class="k"&gt;endblock&lt;/span&gt; &lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything should now be in place and you can set encore running and watching for changes by running this in a terminal window.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn encore dev &lt;span class="nt"&gt;--watch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this is running, you can modify any of the CSS classes in the above template i.e. change &lt;code&gt;text-4xl&lt;/code&gt; to &lt;code&gt;text-6xl&lt;/code&gt; and you should see notifications on the CSS being rebuilt in the terminal window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Viewing your first page
&lt;/h2&gt;

&lt;p&gt;We need to actually see the page to verify it is working as we expect.  For simplicity, I'm going to use PHP's built-in webserver in this example so we can run, in another terminal window&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;public
php &lt;span class="nt"&gt;-S&lt;/span&gt; localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can open our favorite browser and go to &lt;code&gt;http://localhost:8080&lt;/code&gt; and see the result.  It should look something like this&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcrthlw63t2kml7vxzq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7jcrthlw63t2kml7vxzq.png" alt="Symfony project using TailwindCSS " width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try before you buy
&lt;/h2&gt;

&lt;p&gt;So you can see this in action and experiment for yourself, I've created a git repository with the step of this blog post already completed so you can simply &lt;code&gt;git clone&lt;/code&gt; and install the dependencies and away you go.  &lt;/p&gt;

&lt;p&gt;Git repository: &lt;a href="https://github.com/chrisshennan/symfony-and-tailwindcss-example" rel="noopener noreferrer"&gt;https://github.com/chrisshennan/symfony-and-tailwindcss-example&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Get started with Tailwind CSS - Installation - &lt;a href="https://tailwindcss.com/docs/installation" rel="noopener noreferrer"&gt;https://tailwindcss.com/docs/installation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install Tailwind CSS with Laravel - &lt;a href="https://tailwindcss.com/docs/guides/laravel" rel="noopener noreferrer"&gt;https://tailwindcss.com/docs/guides/laravel&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tailwind UI - Official Tailwind CSS Components &amp;amp; Templates - &lt;a href="https://tailwindui.com/" rel="noopener noreferrer"&gt;https://tailwindui.com/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="434" height="100"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Originally published at &lt;a href="https://chrisshennan.com/blog/using-tailwindcss-with-symfony-encore" rel="noopener noreferrer"&gt;https://chrisshennan.com/blog/using-tailwindcss-with-symfony-encore&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>symfony</category>
      <category>webpack</category>
      <category>guide</category>
    </item>
    <item>
      <title>Docker - Tail `docker logs` from the end of the log file</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Fri, 20 May 2022 08:45:00 +0000</pubDate>
      <link>https://forem.com/chrisshennan/docker-tail-docker-logs-from-the-end-of-the-log-file-2d1g</link>
      <guid>https://forem.com/chrisshennan/docker-tail-docker-logs-from-the-end-of-the-log-file-2d1g</guid>
      <description>&lt;p&gt;Many docker containers output log and error information to &lt;code&gt;/dev/stdout&lt;/code&gt; and &lt;code&gt;/dev/stderr&lt;/code&gt; rather than their traditional log files, and you can use the &lt;code&gt;docker logs&lt;/code&gt; command to view those messages.  You can make use of the &lt;code&gt;docker logs&lt;/code&gt; command like this:-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="o"&gt;[&lt;/span&gt;CONTAINER_ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to follow the logs live as you run your application, you can use:-&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;CONTAINER_ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, the &lt;code&gt;docker logs&lt;/code&gt; command shows all the log entries for that container, and if the container has been running for a while, that can be many lines of output that you are not really interested in.  If you are attempting to debug your application, you might want to follow the log entries but beginning now rather than showing everything that came before, i.e. tail the log entries from the end of the log file rather than the start.&lt;/p&gt;

&lt;p&gt;You can do this by using &lt;code&gt;--tail 0&lt;/code&gt; i.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt; 0 &lt;span class="o"&gt;[&lt;/span&gt;CONTAINER_ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, if you want to look back over recent entries, you can set the value accordingly, i.e. to view the last 100 entries you can do&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;--follow&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt; 100 &lt;span class="o"&gt;[&lt;/span&gt;CONTAINER_ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use the shorthand version by replacing &lt;code&gt;--follow&lt;/code&gt; with &lt;code&gt;-f&lt;/code&gt; and &lt;code&gt;--tail&lt;/code&gt; with &lt;code&gt;-n&lt;/code&gt; like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 100 &lt;span class="o"&gt;[&lt;/span&gt;CONTAINER_ID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="434" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>tutorial</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>4 steps to improve Laravel + Docker performance issues</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Tue, 04 Jan 2022 11:31:38 +0000</pubDate>
      <link>https://forem.com/chrisshennan/4-steps-to-improve-laravel-docker-performance-issues-2eif</link>
      <guid>https://forem.com/chrisshennan/4-steps-to-improve-laravel-docker-performance-issues-2eif</guid>
      <description>&lt;p&gt;Ok, so the title is a little misleading.  I was attempting to improve the performance of a Laravel application that was running inside docker but the improvements I made would work with other frameworks i.e. Symfony, CodeIgniter or even your own bespoke application.  In this particular case, these changes resulted in a performance gain of almost &lt;strong&gt;90%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The performance improvements that I introduced were (I'll cover each of these in a bit more detail below).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking for DNS issues&lt;/li&gt;
&lt;li&gt;Installing PHP Opcache&lt;/li&gt;
&lt;li&gt;Configuring Nginx to handle OPTIONS requests&lt;/li&gt;
&lt;li&gt;Installing docker-sync&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Checking for DNS issues
&lt;/h2&gt;

&lt;p&gt;DNS resolution issues can greatly affect the performance of your application and can leave you trying to debug applications for performance issues when your application is working fine.  In my case, I traced the DNS resolution issues down to the use of a &lt;code&gt;.test&lt;/code&gt; domain extension, i.e. example.domain.test.   To try and identify the root cause, I created some sample entries in the &lt;code&gt;/etc/hosts&lt;/code&gt; file to test with i.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;127.0.0.1&lt;/span&gt;   &lt;span class="err"&gt;example.domain.test&lt;/span&gt;
&lt;span class="err"&gt;127.0.0.1&lt;/span&gt;   &lt;span class="err"&gt;example.domain.xyz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these set up, &lt;code&gt;localhost&lt;/code&gt;, &lt;code&gt;example.domain.test&lt;/code&gt; and &lt;code&gt;example.domain.xyz&lt;/code&gt; will all resolve to 127.0.0.1.  I was suspecting performance issues with &lt;code&gt;example.domain.test&lt;/code&gt; and I set up &lt;code&gt;example.domain.xyz&lt;/code&gt; as an alternative mapping to see if the issue was isolated to the &lt;code&gt;.test&lt;/code&gt; extension or also affected the &lt;code&gt;.xyz&lt;/code&gt; one.&lt;/p&gt;

&lt;p&gt;Next, I spun up a standard nginx docker container so there was some content being served for our test cURL request to receive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8081:80 nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this running, we can make a request to &lt;a href="http://localhost:8081" rel="noopener noreferrer"&gt;http://localhost:8081&lt;/a&gt; or &lt;a href="http://example.domain.test:8081" rel="noopener noreferrer"&gt;http://example.domain.test:8081&lt;/a&gt; or &lt;a href="http://example.domain.xyz:8081" rel="noopener noreferrer"&gt;http://example.domain.xyz:8081&lt;/a&gt;) and we should be presented with the default "Welcome to nginx!" page.&lt;/p&gt;

&lt;p&gt;Now we can test if there are any DNS resolution issues by timing the requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Via localhost
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;sh-3.2$&lt;/span&gt; &lt;span class="err"&gt;/usr/bin/time&lt;/span&gt; &lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;-I&lt;/span&gt;  &lt;span class="err"&gt;localhost:8081&lt;/span&gt;
&lt;span class="err"&gt;HTTP/1.1&lt;/span&gt; &lt;span class="err"&gt;200&lt;/span&gt; &lt;span class="err"&gt;OK&lt;/span&gt;
&lt;span class="err"&gt;Server:&lt;/span&gt; &lt;span class="err"&gt;nginx/1.21.4&lt;/span&gt;
&lt;span class="err"&gt;Date:&lt;/span&gt; &lt;span class="err"&gt;Fri,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt; &lt;span class="err"&gt;Dec&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;08:32:50&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;text/html&lt;/span&gt;
&lt;span class="err"&gt;Content-Length:&lt;/span&gt; &lt;span class="err"&gt;615&lt;/span&gt;
&lt;span class="err"&gt;Last-Modified:&lt;/span&gt; &lt;span class="err"&gt;Tue,&lt;/span&gt; &lt;span class="err"&gt;02&lt;/span&gt; &lt;span class="err"&gt;Nov&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;14:49:22&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Connection:&lt;/span&gt; &lt;span class="err"&gt;keep-alive&lt;/span&gt;
&lt;span class="err"&gt;ETag:&lt;/span&gt; &lt;span class="err"&gt;"61814ff2-267"&lt;/span&gt;
&lt;span class="err"&gt;Accept-Ranges:&lt;/span&gt; &lt;span class="err"&gt;bytes&lt;/span&gt;

        &lt;span class="err"&gt;0.01&lt;/span&gt; &lt;span class="err"&gt;real&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;user&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;sys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Via example.domain.test
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;sh-3.2$&lt;/span&gt; &lt;span class="err"&gt;/usr/bin/time&lt;/span&gt; &lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;-I&lt;/span&gt;  &lt;span class="err"&gt;example.domain.test:8081&lt;/span&gt;
&lt;span class="err"&gt;HTTP/1.1&lt;/span&gt; &lt;span class="err"&gt;200&lt;/span&gt; &lt;span class="err"&gt;OK&lt;/span&gt;
&lt;span class="err"&gt;Server:&lt;/span&gt; &lt;span class="err"&gt;nginx/1.21.4&lt;/span&gt;
&lt;span class="err"&gt;Date:&lt;/span&gt; &lt;span class="err"&gt;Fri,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt; &lt;span class="err"&gt;Dec&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;08:33:17&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;text/html&lt;/span&gt;
&lt;span class="err"&gt;Content-Length:&lt;/span&gt; &lt;span class="err"&gt;615&lt;/span&gt;
&lt;span class="err"&gt;Last-Modified:&lt;/span&gt; &lt;span class="err"&gt;Tue,&lt;/span&gt; &lt;span class="err"&gt;02&lt;/span&gt; &lt;span class="err"&gt;Nov&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;14:49:22&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Connection:&lt;/span&gt; &lt;span class="err"&gt;keep-alive&lt;/span&gt;
&lt;span class="err"&gt;ETag:&lt;/span&gt; &lt;span class="err"&gt;"61814ff2-267"&lt;/span&gt;
&lt;span class="err"&gt;Accept-Ranges:&lt;/span&gt; &lt;span class="err"&gt;bytes&lt;/span&gt;

        &lt;span class="err"&gt;5.10&lt;/span&gt; &lt;span class="err"&gt;real&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;user&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;sys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Via example.domain.xyz
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;sh-3.2$&lt;/span&gt; &lt;span class="err"&gt;/usr/bin/time&lt;/span&gt; &lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;-I&lt;/span&gt;  &lt;span class="err"&gt;example.domain.xyz:8081&lt;/span&gt;
&lt;span class="err"&gt;HTTP/1.1&lt;/span&gt; &lt;span class="err"&gt;200&lt;/span&gt; &lt;span class="err"&gt;OK&lt;/span&gt;
&lt;span class="err"&gt;Server:&lt;/span&gt; &lt;span class="err"&gt;nginx/1.21.4&lt;/span&gt;
&lt;span class="err"&gt;Date:&lt;/span&gt; &lt;span class="err"&gt;Fri,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt; &lt;span class="err"&gt;Dec&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;08:33:42&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;text/html&lt;/span&gt;
&lt;span class="err"&gt;Content-Length:&lt;/span&gt; &lt;span class="err"&gt;615&lt;/span&gt;
&lt;span class="err"&gt;Last-Modified:&lt;/span&gt; &lt;span class="err"&gt;Tue,&lt;/span&gt; &lt;span class="err"&gt;02&lt;/span&gt; &lt;span class="err"&gt;Nov&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;14:49:22&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Connection:&lt;/span&gt; &lt;span class="err"&gt;keep-alive&lt;/span&gt;
&lt;span class="err"&gt;ETag:&lt;/span&gt; &lt;span class="err"&gt;"61814ff2-267"&lt;/span&gt;
&lt;span class="err"&gt;Accept-Ranges:&lt;/span&gt; &lt;span class="err"&gt;bytes&lt;/span&gt;

        &lt;span class="err"&gt;0.05&lt;/span&gt; &lt;span class="err"&gt;real&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;user&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;sys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my case, I was running this on Mac OSX and &lt;code&gt;localhost&lt;/code&gt; and &lt;code&gt;example.domain.xyz&lt;/code&gt; returns in a few milliseconds but &lt;code&gt;example.domain.test&lt;/code&gt; has a 5-second delay.  This turned out to be due to attempts to resolve the hostname via IPV6 and I'd only defined the IPV4 mapping in &lt;code&gt;/etc/hosts&lt;/code&gt; file. Once I added the IPV6 mapping into my &lt;code&gt;/etc/hosts&lt;/code&gt; file, like below,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;127.0.0.1&lt;/span&gt;   &lt;span class="err"&gt;example.domain.test&lt;/span&gt;
&lt;span class="err"&gt;127.0.0.1&lt;/span&gt;   &lt;span class="err"&gt;example.domain.xyz&lt;/span&gt;

&lt;span class="err"&gt;::1&lt;/span&gt; &lt;span class="err"&gt;example.domain.test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I could then see that the cURL request to &lt;code&gt;example.domain.test&lt;/code&gt; were resolving quickly as expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;sh-3.2$&lt;/span&gt; &lt;span class="err"&gt;/usr/bin/time&lt;/span&gt; &lt;span class="err"&gt;curl&lt;/span&gt; &lt;span class="err"&gt;-I&lt;/span&gt;  &lt;span class="err"&gt;example.domain.test:8081&lt;/span&gt;
&lt;span class="err"&gt;HTTP/1.1&lt;/span&gt; &lt;span class="err"&gt;200&lt;/span&gt; &lt;span class="err"&gt;OK&lt;/span&gt;
&lt;span class="err"&gt;Server:&lt;/span&gt; &lt;span class="err"&gt;nginx/1.21.4&lt;/span&gt;
&lt;span class="err"&gt;Date:&lt;/span&gt; &lt;span class="err"&gt;Fri,&lt;/span&gt; &lt;span class="err"&gt;17&lt;/span&gt; &lt;span class="err"&gt;Dec&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;08:37:11&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Content-Type:&lt;/span&gt; &lt;span class="err"&gt;text/html&lt;/span&gt;
&lt;span class="err"&gt;Content-Length:&lt;/span&gt; &lt;span class="err"&gt;615&lt;/span&gt;
&lt;span class="err"&gt;Last-Modified:&lt;/span&gt; &lt;span class="err"&gt;Tue,&lt;/span&gt; &lt;span class="err"&gt;02&lt;/span&gt; &lt;span class="err"&gt;Nov&lt;/span&gt; &lt;span class="err"&gt;2021&lt;/span&gt; &lt;span class="err"&gt;14:49:22&lt;/span&gt; &lt;span class="err"&gt;GMT&lt;/span&gt;
&lt;span class="err"&gt;Connection:&lt;/span&gt; &lt;span class="err"&gt;keep-alive&lt;/span&gt;
&lt;span class="err"&gt;ETag:&lt;/span&gt; &lt;span class="err"&gt;"61814ff2-267"&lt;/span&gt;
&lt;span class="err"&gt;Accept-Ranges:&lt;/span&gt; &lt;span class="err"&gt;bytes&lt;/span&gt;

        &lt;span class="err"&gt;0.01&lt;/span&gt; &lt;span class="err"&gt;real&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;user&lt;/span&gt;         &lt;span class="err"&gt;0.00&lt;/span&gt; &lt;span class="err"&gt;sys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: If your application is an API that is accessed via an AJAX request, you could be affected by this delay twice, once for the preflight (OPTIONS) request and again for the GET / POST request, so this change could save you around 10 seconds overall per AJAX request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing PHP Opcache
&lt;/h2&gt;

&lt;p&gt;The PHP docker image doesn't have &lt;code&gt;opcache&lt;/code&gt; enabled by default.  You can add this to your &lt;code&gt;Dockerfile&lt;/code&gt; in a similar way to below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; php:7.4-fpm&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install &lt;/span&gt;opcache &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unless you have specific needs, just enabling &lt;code&gt;opcache&lt;/code&gt; should be enough&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring Nginx to handle OPTIONS requests
&lt;/h2&gt;

&lt;p&gt;If your application is being accessed via an AJAX request then there are 2 requests that are being made.  An &lt;code&gt;OPTIONS&lt;/code&gt; (CORS) request (to see if the action is allowed) and the &lt;code&gt;GET / POST / PATCH&lt;/code&gt; etc request (the actual action) and if your application is handling the &lt;code&gt;OPTIONS&lt;/code&gt; request then you have a delay as a result of passing the request over to PHP and PHP booting up the framework etc to process the request.  &lt;/p&gt;

&lt;p&gt;In a development environment, you might be happy to leave your application open to all requests, in which case we can configure Nginx to handle the &lt;code&gt;OPTIONS&lt;/code&gt; request directly and cut out your application which will speed up the request.  To achieve this we need to add the following into your nginx configuration&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# for OPTIONS return these headers and HTTP 200 status&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;OPTIONS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, in our garage API case, a full &lt;code&gt;server&lt;/code&gt; block for development might look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;root&lt;/span&gt; &lt;span class="n"&gt;/var/www/garage-api/public&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt; &lt;span class="s"&gt;index.html&lt;/span&gt; &lt;span class="s"&gt;index.htm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.php&lt;/span&gt;&lt;span class="nv"&gt;$is_args$args&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;gzip_static&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;# for OPTIONS return these headers and HTTP 200 status&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;OPTIONS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_split_path_info&lt;/span&gt; &lt;span class="s"&gt;^(.+&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.php)(/.+)&lt;/span&gt;$&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="nf"&gt;garage-api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;PATH_INFO&lt;/span&gt; &lt;span class="nv"&gt;$fastcgi_path_info&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installing docker-sync
&lt;/h2&gt;

&lt;p&gt;Install &lt;code&gt;docker-sync&lt;/code&gt; using the following command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;docker-sync
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure&lt;/p&gt;

&lt;h3&gt;
  
  
  docker-sync.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2"&lt;/span&gt;
&lt;span class="na"&gt;syncs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;garage-api-sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./application'&lt;/span&gt;
    &lt;span class="na"&gt;sync_userid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
    &lt;span class="na"&gt;sync_groupid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;33&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  docker-compose.yml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.7'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;garage-api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;volume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s"&gt;garage-api-sync:/app:nocopy&lt;/span&gt;

  &lt;span class="na"&gt;garage-nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Spinning up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;docker-sync start&lt;/span&gt;
&lt;span class="s"&gt;docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Although I was looking to boost the performance of my Laravel application running inside docker, these changes are not related to Laravel and so could be applied to any other framework or PHP application but these 4 changes did have a significant impact on the performance of my application.  &lt;/p&gt;

&lt;p&gt;I focused on one particularly heavy area of the application in which had a frontend application was making 18 API calls (so 9 preflight requests + 9 GET requests) and originally this took around &lt;strong&gt;32 seconds&lt;/strong&gt; to complete all the API calls.  After the changes had been put in place this dropped to around &lt;strong&gt;3.5 seconds&lt;/strong&gt; resulting in a performance boost of almost &lt;strong&gt;90%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker Sync documentation - &lt;a href="https://docker-sync.readthedocs.io/en/latest/index.html" rel="noopener noreferrer"&gt;https://docker-sync.readthedocs.io/en/latest/index.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="434" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>nginx</category>
      <category>docker</category>
      <category>performance</category>
    </item>
    <item>
      <title>Fixing "Authentication plugin 'caching_sha2_password' cannot be loaded" errors</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Mon, 20 Dec 2021 09:31:48 +0000</pubDate>
      <link>https://forem.com/chrisshennan/fixing-authentication-plugin-cachingsha2password-cannot-be-loaded-errors-4g0c</link>
      <guid>https://forem.com/chrisshennan/fixing-authentication-plugin-cachingsha2password-cannot-be-loaded-errors-4g0c</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;You have installed MySQL 8 and are unable to connect your database using your MySQL client (Sequel Pro, HeidiSQL etc).  Every attempt to connect using your MySQL client results in the following error&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication plugin 'caching_sha2_password' cannot be loaded: dlopen(/usr/local/mysql/lib/plugin/caching_sha2_password.so, 2): image not found&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication plugin 'caching_sha2_password' cannot be loaded. The specific module can not be found&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reason
&lt;/h2&gt;

&lt;p&gt;As of MySQL 8.0, &lt;code&gt;caching_sha2_password&lt;/code&gt; is now the default authentication plugin rather than &lt;code&gt;mysql_native_password&lt;/code&gt; which was the default in previous versions.  This means that clients (Sequel Pro, HeidiSQL etc) that rely on the &lt;code&gt;mysql_native_password&lt;/code&gt; won't be able to connect because of this change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolution
&lt;/h2&gt;

&lt;p&gt;1) You can, at a server level, revert to the &lt;code&gt;mysql_native_password&lt;/code&gt; mechanism by adding the following to your MySQL configuration files&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;mysqld]
&lt;span class="nv"&gt;default_authentication_plugin&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mysql_native_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) You can, at a user level, revert to the &lt;code&gt;mysql_native_password&lt;/code&gt; mechanism via the following process&lt;/p&gt;

&lt;p&gt;Open a terminal window and connect to your MySQL instance via the command line&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;USERNAME] &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter your MySQL password and press enter and you should be logged into your MySQL instance.&lt;/p&gt;

&lt;p&gt;Now run the following SQL command, replacing &lt;code&gt;[USERNAME]&lt;/code&gt;, &lt;code&gt;[PASSWORD]&lt;/code&gt; and &lt;code&gt;[HOST]&lt;/code&gt; as appropriate.  &lt;/p&gt;

&lt;p&gt;Note: &lt;code&gt;[HOST]&lt;/code&gt; can be the IP address of your computer which would allow access from your computer only or, in the case of a local development environment, you can use &lt;code&gt;%&lt;/code&gt; to allow from any host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'[USERNAME]'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'[HOST]'&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;mysql_native_password&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'[PASSWORD]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'[USERNAME]'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'%'&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;mysql_native_password&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'[PASSWORD]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you should be able to go back to your MySQL client and connect as normal.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;2.11.4 Changes in MySQL 8.0 - &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html" rel="noopener noreferrer"&gt;https://dev.mysql.com/doc/refman/8.0/en/upgrading-from-previous-series.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;6.4.1.2 Caching SHA-2 Pluggable Authentication - &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html" rel="noopener noreferrer"&gt;https://dev.mysql.com/doc/refman/8.0/en/caching-sha2-pluggable-authentication.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="434" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mysql</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Connect to a private RDS instance using SSH &amp; AWS SSM (Systems Manager)</title>
      <dc:creator>Chris Shennan</dc:creator>
      <pubDate>Wed, 26 May 2021 11:35:37 +0000</pubDate>
      <link>https://forem.com/chrisshennan/connect-to-a-private-rds-instance-using-ssh-aws-ssm-systems-manager-22j0</link>
      <guid>https://forem.com/chrisshennan/connect-to-a-private-rds-instance-using-ssh-aws-ssm-systems-manager-22j0</guid>
      <description>&lt;h2&gt;
  
  
  Objective
&lt;/h2&gt;

&lt;p&gt;You have an AWS RDS instance which is on a private subnet and therefore not publicly accessible and you wish to connect to this RDS instance remotely without installing a bastion server or having any public-facing ec2 instances.&lt;/p&gt;

&lt;p&gt;For this article, we are making the following assumptions:-&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There is no bastion instance to provide external access&lt;/li&gt;
&lt;li&gt;There are no security groups which provide external access&lt;/li&gt;
&lt;li&gt;There is an ec2 instance in a private subnet which has access to the RDS instance&lt;/li&gt;
&lt;li&gt;This ec2 instance is running Ubuntu&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;ec2-instance-connect&lt;/code&gt; on EC2 instance - This can be done by connecting to an existing ec2 instance using AWS Session Manager and run
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  apt-get &lt;span class="nb"&gt;install &lt;/span&gt;ec2-instance-connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;Note: If you are using autoscaling groups or blue/green deployments, you can add this to the ec2 user data make sure that &lt;code&gt;ec2-instance-connect&lt;/code&gt; is installed when a new server is created.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;session-manager-plugin&lt;/code&gt; locally (&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create an SSH Key
&lt;/h3&gt;

&lt;p&gt;You will need an SSH to connect to the ec2 instance.  If you already have one you wish to use then you can skip to the next step, otherwise, you can create one via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-f&lt;/span&gt; my_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create SSH private and public keys named &lt;code&gt;my_rsa&lt;/code&gt; and &lt;code&gt;my_rsa.pub&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push your SSH Key to the target instance
&lt;/h3&gt;

&lt;p&gt;To connect to the ec2 instance, you will need to send the SSH public key to the desired ec2 instance via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2-instance-connect send-ssh-public-key &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0c6e3bd52bbb2373c &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--availability-zone&lt;/span&gt; eu-west-1a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-os-user&lt;/span&gt; ubuntu &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssh-public-key&lt;/span&gt; file:///my_rsa.pub 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: The SSH public keys are only available for one-time use for 60 seconds in the instance metadata. To connect to the instance successfully, you must connect using SSH within this time window. Because the keys expire, there is no need to track or manage these keys directly, as you did previously.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Session
&lt;/h3&gt;

&lt;p&gt;You can now start an AWS System Manager session and enable port forwarding.  In this case, you are going to forward port &lt;code&gt;9999&lt;/code&gt; on your local machine to port &lt;code&gt;22&lt;/code&gt; on the target ec2 instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm start-session &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; i-0c6e3bd52bbb2373c &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--document-name&lt;/span&gt; AWS-StartPortForwardingSession &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="s1"&gt;'{"portNumber":"22", "localPortNumber":"9999"}'&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: This will appear to hang because it is maintaining a tunnelling connection from port &lt;code&gt;9999&lt;/code&gt; on &lt;code&gt;localhost&lt;/code&gt; to port &lt;code&gt;22&lt;/code&gt; on &lt;code&gt;i-07edf50160ab3172&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open tunnel to RDS (in another terminal)
&lt;/h3&gt;

&lt;p&gt;Now you need to open a tunnel to your RDS instance via the AWS System Manager session you created above.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ubuntu@localhost &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-p&lt;/span&gt; 9999 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-L&lt;/span&gt; 3388:production-database.inzy2e1e4v6s.eu-west-1.rds.amazonaws.com:3306
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note:  This will appear to hang because it is maintaining a tunnelling connection between port &lt;code&gt;3388&lt;/code&gt; on &lt;code&gt;localhost&lt;/code&gt; to port &lt;code&gt;3306&lt;/code&gt; on &lt;code&gt;production-database.inzy2e1e4v6s.eu-west-1.rds.amazonaws.com&lt;/code&gt; via &lt;code&gt;i-07edf50160ab3172&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect to your RDS Instance
&lt;/h3&gt;

&lt;p&gt;With the above steps complete, you can now use your favourite database client (SequelPro, HeidiSQL etc) to connect to your database.  The connection details will be:-&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host: &lt;code&gt;localhost&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Port: &lt;code&gt;3388&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Username / Password: as per the live database you are trying to access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should now have access to your RDS instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Using Amazon EC2 Instance Connect for SSH access to your EC2 Instances - &lt;a href="https://aws.amazon.com/blogs/compute/new-using-amazon-ec2-instance-connect-for-ssh-access-to-your-ec2-instances/" rel="noopener noreferrer"&gt;https://aws.amazon.com/blogs/compute/new-using-amazon-ec2-instance-connect-for-ssh-access-to-your-ec2-instances/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install the Session Manager Plugin for the AWS CLI - &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html" rel="noopener noreferrer"&gt;https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you enjoy this article and would like to show your support, you can easily do so by buying me a coffee. Your contribution is greatly appreciated!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/chrisshennan" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.buymeacoffee.com%2Fbuttons%2Fdefault-yellow.png" alt="Buy Me A Coffee" width="434" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ssh</category>
      <category>rds</category>
    </item>
  </channel>
</rss>
