PHP - BlogFlockAll things PHP2026-02-09T05:31:50.182ZBlogFlockPhpStorm : The IDE that empowers PHP developers | The JetBrains Blog, Exakat, Rob Allen, Blog entries :: mwop.netRemoving a filename containing a null byte or binary character on Linux - Blog entries :: mwop.nethttps://mwop.net/blog/2026-02-06-linux-null-byte-filename-removal.html2026-02-06T17:01:27.000Z<p>Somehow, I got a file in my tree that started with a null byte. I only discovered it because <code>git status</code> was noting it wasn't added to my branch, and wanted to know if I wanted to add it.</p>
<p>No, no, I did not. I wanted to delete it.</p>
<p>But, of course, you cannot reference such a file by name, so I had to learn a few tricks.</p>
<p>First off, you can list names with binary bytes using:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">/bin/ls -lb
</code></pre>
<p>But even better, you can get the <em>inode</em> if you use:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">/bin/ls -li
</code></pre>
<p>When you do, the inode is in the first column of the list for each file.</p>
<p>Once you know that, you can delete the file, using <code>find</code>:</p>
<pre><code class="language-bash hljs bash" data-lang="bash">find . -maxdepth 1 -inum <inode_id> -<span class="hljs-built_in">exec</span> rm -i -- {} +
</code></pre>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2026-02-06-linux-null-byte-filename-removal.html">Removing a filename containing a null byte or binary character on Linux</a> was originally
published <time class="dt-published" datetime="2026-02-06T11:01:27-06:00">6 February 2026</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
Dad - Rob Allenhttps://akrabat.com/?p=75382026-01-22T15:00:00.000Z<p>I don't often post personal things here, however last weekend my dad died peacefully in his sleep and it feels important to acknowledge this. </p>
<p>I have no words.</p>
<p></p>
<p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2026/01/2026IMG_8398-web.jpg" alt="Keith Allen, 1932-2026" title="IMG_8398-web.jpg" border="0" style="max-width: 600px" /></p>
2025 in pictures - Rob Allenhttps://akrabat.com/?p=75312025-12-31T17:52:04.000Z<p>As we reach the end of 2025, I take this opportunity to look back over the photos that I have taken and thing about the year. This year I published 1,064 photos to Flickr with, of course, at least one photo every day as part of my <a href="https://project365.akrabat.com/2025.html">Project 365</a>. </p>
<p>The lovely thing is that my photos remind me what happened during my year and I remember many good times through them as I've seen friends and family over the months. I've done for many years now and you can see them all in my <a href="https://akrabat.com/category/year-in-pictures/">Year in pictures</a> category. </p>
<h2>January</h2>
<p>I started the year visiting the <a href="https://svr.co.uk">Severn Valley Railway</a> and took some photos of steam trains. I also visited <a href="https://phpstoke.co.uk">PHP Stoke</a> which was fun.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54245092637" title="View 'Hagley Hall' on Flickr.com"><img fetchpriority="high" decoding="async" alt="Hagley Hall." border="0" height="180" src="https://live.staticflickr.com/65535/54245092637_fbb69df220_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54271380091" title="View 'PHP Stoke user group' on Flickr.com"><img decoding="async" alt="PHP Stoke user group." border="0" height="180" src="https://live.staticflickr.com/65535/54271380091_13ef9bb76a_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">February</h2>
<p>I attended three community events in February, starting with <a href="https://fosdem.org/">FOSDEM</a> at the very start, <a href="https://www.phpconference.co.uk">PHP UK</a> mid-way through, and then <a href="https://www.meetup.com/fusion-technology-meetup-birmingham/">Fusion</a> towards the end. I really like catching up with the community and learning from them in the hallway track! I also photographed planes with Stuart!</p>
<p><a href="https://www.flickr.com/photos/akrabat/54316631185" title="View 'Stuart photographs an Avro Lincoln B2' on Flickr.com"><img decoding="async" alt="Stuart photographs an Avro Lincoln B2." border="0" height="180" src="https://live.staticflickr.com/65535/54316631185_dc8f5f659c_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54342351180" title="View 'Lorna and Ciaran at PHPUK' on Flickr.com"><img loading="lazy" decoding="async" alt="Lorna and Ciaran at PHPUK." border="0" height="180" src="https://live.staticflickr.com/65535/54342351180_608df6f181_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">March</h2>
<p>I had a quick trip to Spain this month which was very cold and wet and then spoke at <a href="https://drupalmountaincamp.ch">Drupal Mountain Camp</a>; my first time attending this conference and I loved the community vibe here. I was also enjoyed my visit to the <a href="https://kwvr.co.uk">KVWR</a> with Kevin.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54393395137" title="View 'Torchlit walk back to Davos' on Flickr.com"><img loading="lazy" decoding="async" alt="Torchlit walk back to Davos." border="0" height="180" src="https://live.staticflickr.com/65535/54393395137_8c7392e73b_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54405743399" title="View '44871' on Flickr.com"><img loading="lazy" decoding="async" alt="" border="0" height="180" src="https://live.staticflickr.com/65535/54405743399_7ff56547c1_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">April</h2>
<p>In April, I went down to London to take care of my dad while my mum visited her sister in America. I fitted in a trip to the SVR Spring Gala, and also spoke at PHP Berkshire, sharing the bill with <a href="https://www.flickr.com/photos/akrabat/54496644289">Dave Liddament</a></p>
<p><a href="https://www.flickr.com/photos/akrabat/54436035565" title="View 'Breakfast at the cafe with Dad' on Flickr.com"><img loading="lazy" decoding="async" alt="Breakfast at the cafe with Dad." border="0" height="180" src="https://live.staticflickr.com/65535/54436035565_c4c635a72e_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54465354171" title="View '75069' on Flickr.com"><img loading="lazy" decoding="async" alt="" border="0" height="180" src="https://live.staticflickr.com/65535/54465354171_34f9118734_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">May</h2>
<p>We celebrated lots of birthdays in May and visited Dudley Canal & Caverns. I spoke at <a href="https://www.phpday.it">phpday</a> in Verona and later the PHP Oxford user group. The community at both events are fantastic and I enjoyed all the great conversations. My wife and I also enjoyed a visit to <a href="https://www.nationaltrust.org.uk/visit/warwickshire/charlecote-park">Charlecote</a>. Most importantly, <a href="https://www.cpfc.co.uk/news/announcement/palaces-historic-fa-cup-triumph-voted-moment-of-the-season/">Crystal Palace won the FA cup</a> this month for the first time in their history!</p>
<p><a href="https://www.flickr.com/photos/akrabat/54542066604" title="View 'Discussions during a break' on Flickr.com"><img loading="lazy" decoding="async" alt="Discussions during a break." border="0" height="180" src="https://live.staticflickr.com/65535/54542066604_d6929e0ea1_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54542276400" title="View 'Charlecote' on Flickr.com"><img loading="lazy" decoding="async" alt="" border="0" height="180" src="https://live.staticflickr.com/65535/54542276400_1ce0118442_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">June</h2>
<p>The Nintendo Switch 2 was released this month and it's a great sequel to the original Switch. We spent most of the month on holiday in Spain, where I ate lots of ice cream.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54570731579" title="View 'New Nintendo Switch 2!' on Flickr.com"><img loading="lazy" decoding="async" alt="New Nintendo Switch 2!." border="0" height="180" src="https://live.staticflickr.com/65535/54570731579_b3a5c372f4_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54635347050" title="View 'Breakfast at Jijonenca' on Flickr.com"><img loading="lazy" decoding="async" alt="Breakfast at Jijonenca." border="0" height="180" src="https://live.staticflickr.com/65535/54635347050_b44e994cf9_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">July</h2>
<p>July was all about family and friends. My aunt and cousins from America visited, so we moved our family meet-up to July. We also had our regular lunch with friends in Crewe as that's half-way between us. We also celebrated our youngest's graduation of a first class degree in mathematics.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54654182619" title="View 'Family' on Flickr.com"><img loading="lazy" decoding="async" alt="" border="0" height="180" src="https://live.staticflickr.com/65535/54654182619_98a1aab8ee_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54676710940" title="View 'Graduation day' on Flickr.com"><img loading="lazy" decoding="async" alt="Graduation day." border="0" height="180" src="https://live.staticflickr.com/65535/54676710940_22a343d7d9_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">August</h2>
<p>There was <a href="https://www.thegreatestgathering.co.uk">a big railway event in August</a> celebrating 200 years of the modern railways. I visited mum & dad and I took dad out for breakfast at the local café which turned out to be the last time that dad and I will do that. We also visited our friend in Estonia.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54697473959" title="View 'So. Many. Trains.' on Flickr.com"><img loading="lazy" decoding="async" alt="So. Many. Trains." border="0" height="180" src="https://live.staticflickr.com/65535/54697473959_3e2d85ed2e_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54718230708" title="View 'Lunch at EuroCafé' on Flickr.com"><img loading="lazy" decoding="async" alt="Lunch at EuroCafé." border="0" height="180" src="https://live.staticflickr.com/65535/54718230708_56a8d0c5a6_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">September</h2>
<p>The big news in September is that my dad's wellbeing deteriorated far enough that mum was no longer able to care for him. He was taken to hospital and is now living in a care home. I'm pleased that the transition seems to have gone smoothly for both of them. Later that month, I attended <a href="https://www.apidays.global/events/london">apidays London</a> and then took a few days to take photos of <a href="https://www.nymr.co.uk">trains in Yorkshire</a>, while catching up with good friends in the evenings. The Moors really are a lovely place to spend time.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54798520592" title="View 'Dad' on Flickr.com"><img loading="lazy" decoding="async" alt="" border="0" height="180" src="https://live.staticflickr.com/65535/54798520592_57430deefd_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54823597257" title="View '257 Squadron on the NYMR' on Flickr.com"><img loading="lazy" decoding="async" alt="257 Squadron on the NYMR." border="0" height="180" src="https://live.staticflickr.com/65535/54823597257_2abe0f92ea_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">October</h2>
<p>In October, my aunt from America visited again which was lovely as she stayed with my mum, arriving a few days after dad had moved into a care home. I also spoke at <a href="https://nordicapis.com/events/platform-summit-2025/">Nordic APIs Platform Summit</a> in Stockholm. I was surprised to be on the main track and the conference as a whole was excellent. Finally, we celebrated another graduation as my wife graduated with a first class degree in Computing.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54865465096" title="View 'Jacob was as insightful as ever' on Flickr.com"><img loading="lazy" decoding="async" alt="Jacob was as insightful as ever." border="0" height="180" src="https://live.staticflickr.com/65535/54865465096_6d0e69268a_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54893106391" title="View 'Georgina's graduation' on Flickr.com"><img loading="lazy" decoding="async" alt="Georgina's graduation." border="0" height="180" src="https://live.staticflickr.com/65535/54893106391_8bcfa8fb49_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">November</h2>
<p>A quiet month, where I spoke at <a href="https://live.symfony.com/2025-amsterdam-con/">SymfonyCon</a> which celebrated the 20th anniversary of the framework.</p>
<p><a href="https://www.flickr.com/photos/akrabat/54955642911" title="View 'Lots of people in my talk' on Flickr.com"><img loading="lazy" decoding="async" alt="Lots of people in my talk." border="0" height="180" src="https://live.staticflickr.com/65535/54955642911_c454bbe7c4_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/54955643966" title="View 'Core Team Q&A' on Flickr.com"><img loading="lazy" decoding="async" alt="Core Team Q&A." border="0" height="180" src="https://live.staticflickr.com/65535/54955643966_e48b24a36d_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<h2 style="clear: both; margin-top: 20px; padding-top: 20px;">December</h2>
<p>I finished the year attending <a href="https://www.apidays.global/events/paris">apidays Paris</a>, my first visit to this very large conference, where I met some amazing people and learned things. We had a lovely Christmas at home where lots of cake and mince pies were consumed. Then, right at the end of the month, I visited the SVR for the final time this year to take photos of heritage diesel locomotives. I also bought a new camera for next year!</p>
<p><a href="https://www.flickr.com/photos/akrabat/54992440556" title="View 'At the API standards booth' on Flickr.com"><img loading="lazy" decoding="async" alt="At the API standards booth." border="0" height="180" src="https://live.staticflickr.com/65535/54992440556_7b81b055b0_n.jpg" style="float:left;" width="320" /></a><a href="https://www.flickr.com/photos/akrabat/55012341009" title="View '50 033 passes Trimpley Reservoir' on Flickr.com"><img loading="lazy" decoding="async" alt="50 033 passes Trimpley Reservoir." border="0" height="180" src="https://live.staticflickr.com/65535/55012341009_d4880a933c_n.jpg" style="float:left;margin-left: 3px;" width="320" /></a></p>
<p style="clear: both; margin-top: 3em; padding-top: 3em">
Looking back over 2025, it was a bittersweet year with some excellent times with family, friends and community folks, but it was tinged with my dad's transition to a care home. As always, I also very much enjoyed my railway photography and trips with Stuart.</p>
<p>Who knows what 2026 will bring, but I will continue to take photos throughout!</p>
Built-in Laravel Support: A New Era for PhpStorm Developers - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=6640372025-12-10T17:02:10.000Z
<p>For years, PhpStorm has been the go-to IDE for PHP developers – powerful and deeply integrated with the language. But there was one thing many kept asking for: Laravel support out of the box.</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">It should come with built-in</p>— Amdadul Haq (@amdad121) <a href="https://twitter.com/amdad121/status/1950653324551958793?ref_src=twsrc%5Etfw" target="_blank" rel="noopener">July 30, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Starting with <strong>PhpStorm 2025.3, Laravel support is now built in</strong>.<strong> </strong>From the moment you open your project, everything just works – no plugins, no configuration, no extra steps.</p>
<p>Discover the story behind this update from PhpStorm Team Lead <a href="https://x.com/pronskiy" target="_blank">Roman Pronskiy</a>, PhpStorm Software Developer Maria Filippova, JetBrains Product Lead <a href="https://x.com/artpestretsov" target="_blank">Artemy Pestretsov</a>, Laravel Idea plugin creator <a href="https://x.com/Adelf32" target="_blank">Adel Faizrakhmanov</a>, <a href="https://x.com/laravelphp" target="_blank">Laravel</a> creator <a href="https://x.com/taylorotwell" target="_blank">Taylor Otwell</a>, and JetBrains Developer Advocate for PHP <a href="https://x.com/brendt_gd" target="_blank">Brent Roose</a>:</p>
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe title="Out-of-the-Box Laravel Support — Now in PhpStorm" src="https://www.youtube.com/embed/AZoOX6_mcZw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>
<div class="buttons">
<div class="buttons__row">
<a href="https://www.jetbrains.com/phpstorm/laravel/" class="btn" target="" rel="noopener">Start your Laravel journey</a>
</div>
</div>
<h2 class="wp-block-heading">FAQ</h2>
<h3 class="wp-block-heading"><strong>I use IntelliJ IDEA. How is Laravel support provided there</strong>?</h3>
<p>In IntelliJ IDEA, you can install the Laravel Idea plugin as usual from the main menu: <em>File | Settings | Plugins | Marketplace → “Laravel Idea”.</em></p>
<h3 class="wp-block-heading"><strong>Will the standalone Laravel Idea plugin continue to be maintained?</strong></h3>
<p>Yes. Adel and the PhpStorm team will continue collaborating to ensure the plugin – and PhpStorm’s integrated support – remain the top-tier Laravel development experience in the industry.</p>
<h3 class="wp-block-heading"><strong><strong>Is Laravel support available if I’m using PhpStorm for free?</strong></strong></h3>
<p>Yes. The built-in Laravel support is available to all PhpStorm users, including those using:</p>
<ul>
<li>The 30-day free trial.</li>
<li>Early access builds.</li>
<li>Free licenses, such as licenses for students, teachers, open-source projects, or startups.</li>
</ul>
<p>If you can run PhpStorm, you can use Laravel support – no limitations.</p>
<h3 class="wp-block-heading"><strong><strong><strong>I use PhpStorm. Can I now uninstall the Laravel Idea plugin?</strong></strong></strong></h3>
<p>In PhpStorm, the Laravel Idea plugin now comes pre-installed and enabled, so you no longer have to download or manage it manually. However, you should keep the plugin enabled; disabling or removing it will turn off all Laravel-specific features.</p>
From zsh to fish - Blog entries :: mwop.nethttps://mwop.net/blog/2025-11-17-from-zsh-to-fish.html2025-11-17T23:00:10.000Z<p>I'm a longtime zsh user. A colleague introduced me to it in 2009, and I was an instant convert, if nothing else than for directory aliases and simpler <code>$PATH</code> management. Within a couple of years, I discovered <a href="https://ohmyz.sh">oh-my-zsh</a>, which put my shell on steroids, giving me a ton of completion capabilities, better prompts, and more.</p>
<p>But a few years ago, I started noticing that my shell load times were getting worse and worse. At that time, I discovered I could easily switch to vanilla zsh with <a href="https://zplug.github.io">zplug</a> managing a small number of plugins I used (nvm, fzf, and a few others). I also discovered <a href="https://startship.rs">starship</a>, which gave me more prompt options, with faster startup times.</p>
<p>And yet...</p>
<p>I kept reading <a href="https://jvns.ca">Julia Evans</a> recommending <a href="https://fishshell.com">fish</a>. She often would note that fish just does things that other shells need plugins or customization for: decent tab completion, better history capabilities, etc.</p>
<p>So I took the plunge finally in the past couple weeks to give fish a try.</p>
<h2>Configuration</h2>
<p>First off, the switch more than halved the amount of configuration I needed to have an equivalent setup. I was able to remove a ton of configuration I had in zsh around history management, autocompletion, and overrides.</p>
<p>As I noted, with zsh, I was using zplug, and I had a half-dozen plugins. With fish, initially I had no plugins, but in order to get <a href="https://github.com/nvm-sh/nvm">nvm</a> running, I needed to install <a href="https://github.com/jorgebucaran/fisher">fisher</a>, the de facto fish plugin manager. But that's literally the only plugin I'm now using.</p>
<p>I continued to use starship and fzf, which meant two lines of configuration in my fish configuration, and no changes otherwise.</p>
<p>And there's no lag whatsoever when starting up a shell. With zsh, even with my minimal config, I would sometimes wait a second or two for a shell to spawn. With fish, no wait.</p>
<h2>Discoveries</h2>
<p>One thing I've kept from oh-my-zsh is a utility called <code>take</code>, which does the following:</p>
<ul>
<li>If given a directory name, it creates it, and then enters it</li>
<li>If given an archive file, it unarchives it into a directory, and then enters that directory</li>
<li>If given a git repository name, it clones it, and then enters that directory</li>
</ul>
<p>When porting this to fish, I discovered some really cool features of that shell.</p>
<p>First, fish will automatically autoload functions from the <code>functions/</code> subdirectory of your fish configuration. So if you name the function the same as the file (e.g., <code>functions/take.fish</code>), it will load it <em>on demand</em>. This is a nice performance improvement over loading <em>everything</em>.</p>
<p>Second, fish uses a standard syntax for any block statement. Instead of sometimes needing braces, sometimes needing a keyword (which generally varies BASED on the block type - e.g. <code>fi</code> to end a conditional, <code>done</code> to end a loop), all blocks use an <code>end</code> keyword. This makes it far simpler to remember and less prone to errors.</p>
<p>Third, when defining a function, you can specify variable names to which to capture arguments. This is far easier to visually parse and use than standard posix shells, where you use positional parameters. As an example:</p>
<pre><code class="language-shell hljs shell" data-lang="shell">function takedir -a newpath
mkdir -p $newpath && cd $newpath
end
</code></pre>
<p>Fourth, while you <em>can</em> use the notation <code>varname=value</code> to define variables, there's a better built-in, the <code>set</code> directive, which can:</p>
<ul>
<li>Define block-local (<code>set -l</code>) and function-local (<code>set -f</code>) variables</li>
<li>Define globally-available variables (<code>set -g</code>)</li>
<li>Define environment variables (<code>set -x</code>, for e<strong>x</strong>port) that persist to child shells</li>
</ul>
<p>Using this, it's far easier both to ensure that a variable is scoped correctly, as well as to reason about the scope of a given variable. And those captured arguments I mentioned? Automatically scoped to the function, so they won't bleed outside of it.</p>
<p>(There's also a "univeral" flag, <code>-u</code>, which will not only set it in the current shell, at the globally available level, but make it available across any other instances, and persist it for future invocations. This seems dangerous, though!)</p>
<p>The combination of these meant that the <code>take</code> declaration took fewer lines of code, was easier to understand, and less likely to bleed state. I'll take it!</p>
<h2>But will I stick with it?</h2>
<p>I think so. I even put it on some servers I maintain, and it's instantly given me more and better functionality than the default shell available on each, which makes being on those servers more comfortable. Having less configuration is something I've been keeping an eye on, as more configuration means it's harder to reason about how things work, and more likely to break or fail in interesting ways when updating or upgrading.</p>
<p>I'll still need to keep my bash chops; provisioning scripts for containers and VMs generally have to depend on this lowest common denominator. However, having a useful out-of-the-box shell for my workstation and servers that's easy to script? I'll take it.</p>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-11-17-from-zsh-to-fish.html">From zsh to fish</a> was originally
published <time class="dt-published" datetime="2025-11-17T17:00:10-06:00">17 November 2025</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
IPTC export changed in Photos for macOS 26 Tahoe - Rob Allenhttps://akrabat.com/?p=75232025-11-11T11:00:00.000Z<p>I've recently upgraded my MacBook Air to macOS 26 Tahoe and one thing I noticed was that <a href="https://github.com/akrabat/rodeo">Rodeo</a>'s rules were no longer working. With the help of <a href="https://exiftool.org">exiftool</a>, I worked out that when exporting to JPEG from Photos for macOS 26 Tahoe, the <tt>Object Name</tt>, <tt>Caption-Abstract</tt> and <tt>Keywords</tt> IPRC properties were no longer being populated. This is a regression from macOS 15 Sequoia.</p>
<p>This is the data from <tt>exiftool</tt>.</p>
<p>Output from macOS 15 Sequoia:</p>
<pre lang="sh">
$ exiftool -f -ObjectName -Caption-Abstract -Keywords IMG_8016.jpeg
Object Name : Replaced the broken nose pad on my glasses
Caption-Abstract : It's a nuisance when the nosepad breaks!
Keywords : glasses, Project 365, 365:2025
</pre>
<p>Output from macOS 26.1:</p>
<pre lang="sh">
$ exiftool -f -ObjectName -Caption-Abstract -Keywords IMG_8016.jpeg
Object Name : -
Caption-Abstract : -
Keywords : -
</pre>
<p>This is obviously, less than helpful if your application reads these fields!</p>
<p>Fortunately, the export from macOS 26 Tahoe does fill in some other fields with this data: <tt>Title</tt>, <tt>Description</tt> and <tt>Subject</tt>. These ones are populated by both macOS 15 and 26:</p>
<p>Output from macOS 15 Sequoia:</p>
<pre lang="sh">
$ exiftool -f -Title -Description -Subject IMG_8016.jpeg
Title : Replaced the broken nose pad on my glasses
Description : It's a nuisance when the nosepad breaks!
Subject : glasses, Project 365, 365:2025
</pre>
<p>Output from macOS 26.1:</p>
<pre lang="sh">
$ exiftool -f -Title -Description -Subject IMG_8016.jpeg
Title : Replaced the broken nose pad on my glasses
Description : It's a nuisance when the nosepad breaks!
Subject : glasses, Project 365, 365:2025
</pre>
<p>I checked the IPTC <a href="https://iptc.org/standards/photo-metadata/reference-images/">reference images</a> and they populate <tt>Object Name</tt>, <tt>Caption-Abstract</tt> and <tt>Keyword</tt> in the same way as macOS 15 Sequoia does, so I'm at a loss as to why this is broken in the new macOS 26.</p>
<p>Regardless, I have updated Rodeo appropriately, and <a href="https://github.com/akrabat/rodeo/releases/tag/0.5">version 0.5</a> resolves this problem!</p>
PhpStorm Plugins You Might Not Know - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=6555352025-11-04T16:15:49.000Z
<p>PhpStorm comes with a ton of built-in features, and you can add even more with <a href="https://plugins.jetbrains.com/phpstorm" target="_blank" rel="noopener">plugins</a>. They bring new languages, tools, and small improvements that make everyday coding smoother.</p>
<p>Many of the best ones come from independent developers. One of them is <a href="https://github.com/xepozz" target="_blank" rel="noopener"><strong>Dmitrii Derepko</strong></a>, who built several handy tools for working with web projects and PHP. Below are a few of his creations worth checking out.</p>
<blockquote class="wp-block-quote">
<p>If you’re curious about plugin development, don’t miss the <a href="https://lp.jetbrains.com/plugin-developer-conf-2025/" target="_blank" rel="noopener"><strong>JetBrains Plugin Developer Conference 2025 </strong></a> – free online conference. Dmitrii will be speaking there and will walk you through plugin creation basics: from the initial setup to releasing it on JetBrains Marketplace.</p>
</blockquote>
<h2 class="wp-block-heading">.gitattributes support</h2>
<figure class="wp-block-image size-full"><img decoding="async" fetchpriority="high" width="1600" height="1388" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-12.png" alt="" class="wp-image-655647"/></figure>
<p>If you’ve ever worked with libraries distributed through Composer, NPM, or other package managers, you’ve probably seen a <code>.gitattributes</code> file. It controls how your project looks when archived (via git archive) and which files get included or excluded when users install your package.</p>
<p>The <a href="https://plugins.jetbrains.com/plugin/26477--gitattributes-support" target="_blank" rel="noopener"><strong>.gitattributes Support</strong></a> plugin adds first-class editing experience for this file in PhpStorm. It highlights syntax, recognizes attribute rules, and even suggests valid options while you type so you don’t have to look them up each time. Small details, but they make editing smoother and help prevent mistakes.</p>
<p>🔗 <a href="https://plugins.jetbrains.com/plugin/26477--gitattributes-support" target="_blank" rel="noopener">Plugin Marketplace</a><br>💻 <a href="https://github.com/j-plugins/gitattributes-plugin" target="_blank" rel="noopener">Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">Git Codeowners</h2>
<figure class="wp-block-image size-full"><img decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-10.png" alt="" class="wp-image-655555"/></figure>
<p>If you use GitHub or GitLab, you’ve probably seen a <code>CODEOWNERS</code> file. It tells your platform who’s responsible for reviewing pull requests that touch specific parts of the codebase.</p>
<p>The Git CODEOWNERS plugin makes editing this file painless. It autocompletes file paths, suggests team names, and validates syntax to prevent mistakes before you commit.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/26491-git-codeowners" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/git-codeowners-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">Cron / Crontab Support</h2>
<figure class="wp-block-image size-full"><img decoding="async" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-11.png" alt="" class="wp-image-655602"/></figure>
<p>Cron is the classic tool for running tasks on a schedule. But remembering its five-field syntax is… painful.</p>
<p>This plugin highlights errors in your cron expressions, translates them into plain English (“runs every Monday at 3 AM”), and even lets you execute commands directly from PhpStorm – no need to switch to the terminal.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/26412-cron--crontab-support" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/crontab-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">Sitemap Support</h2>
<figure class="wp-block-image size-full"><img decoding="async" width="1600" height="1180" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-13.png" alt="" class="wp-image-655660"/></figure>
<p>Sitemaps tell search engines which URLs your site exposes and when they were last updated.</p>
<p>This plugin gives you a table-like XML editor for editing and inspecting sitemap files. You can view indexed vs. non-indexed pages, search or filter entries, and tweak URLs by hand if your generator misses something.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/26537-sitemap" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/sitemap-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">TempestPHP</h2>
<p>A work-in-progress plugin that adds support for <a href="https://github.com/tempestphp/tempest-framework" target="_blank" rel="noopener">Tempest</a> (an upcoming new PHP framework) to PhpStorm. Features include support for its custom view syntax, route- and view file support, autocompletion for database models, and more.</p>
<p>🔗 <a href="https://plugins.jetbrains.com/plugin/28504-tempest-php" target="_blank" rel="noopener">Plugin Marketplace</a> <br>💻 <a href="https://github.com/tempestphp/tempest-phpstorm-plugin" target="_blank" rel="noopener">Source code on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">Buggregator</h2>
<figure class="wp-block-image size-full"><img decoding="async" width="1600" height="1177" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-14.png" alt="" class="wp-image-655671"/></figure>
<p>Buggregator is a visual debugging toolkit that lets you inspect data dumps right inside PhpStorm. </p>
<p>Just call <code>trap()</code> in your PHP code, and the plugin automatically spins up a local server that listens for these calls and renders the data in a docked panel — no browser tabs, no context switching.<br><br>It also works seamlessly with the libraries you already use, like Ray, Symfony/VarDumper, Monolog, Sentry, etc.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/26344-buggregator" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/buggregator/phpstorm-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">PHP Dump</h2>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="1189" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-16.png" alt="" class="wp-image-655696"/></figure>
<p>PHP Dump builds on PHP Opcodes Language to visualize your code’s compiled output in real time.</p>
<p>It tokenizes the current file, shows the AST (Abstract Syntax Tree), and displays Opcache details. It helps you understand why a specific function might run slower than expected, without ever executing it.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/28100-php-dump" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/php-dump-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">FileSystem Info</h2>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="1001" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-15.png" alt="" class="wp-image-655683"/></figure>
<p>Sometimes you just want to know which files in your project are taking up space or sitting forgotten.</p>
<p>FileSystem Info scans your project tree and adds file sizes in the project structure tree so you can quickly spot large files.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/28300-filesystem-info" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/fs-info-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">Git Churn</h2>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="1140" src="https://blog.jetbrains.com/wp-content/uploads/2025/11/image-9.png" alt="" class="wp-image-655536"/></figure>
<p>Git Churn shows you which files in your repository change too often or never change at all.</p>
<p>Frequent edits usually signal overly complex or unstable code. This plugin helps you spot those hotspots so you can refactor with purpose instead of guesswork.</p>
<p>🔗<a href="https://plugins.jetbrains.com/plugin/28319-git-churn" target="_blank" rel="noopener"> Plugin Marketplace</a><br>💻<a href="https://github.com/j-plugins/git-churn-plugin" target="_blank" rel="noopener"> Source on GitHub</a></p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<p></p>
<p>That’s it for this roundup – a few plugins that make PhpStorm just a bit smarter.</p>
<p>If you ever had an idea for a PhpStorm plugin or run into something that could be automated or improved, drop it in the comments – maybe Dmitrii (or someone else from the community) will take it on next!</p>
CSS Feature Selectors - Blog entries :: mwop.nethttps://mwop.net/blog/2025-11-03-css-feature-selectors.html2025-11-03T20:09:00.000Z<p>I have a number of locations on my website where I've been faking a <a href="https://masonry.desandro.com/">masonry layout</a> (you know, like how Pinterest lays things out) using CSS grid columns. They work, but they're not what I'm looking for.</p>
<p>I've been waiting for the official <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Masonry_layout">masonry grid display</a> to land, and discovered that it's now an experimental feature in Chrome-based browsers that you can enable (it's been available for Firefox for awhile).</p>
<p>But... how can I check to see if the feature exists? Turns out CSS already has that covered, via the <code>@supports</code> selector:</p>
<pre><code class="language-css hljs css" data-lang="css"><span class="hljs-keyword">@supports</span> (<span class="hljs-attribute">display:</span> masonry) {
<span class="hljs-selector-class">.container</span> {
<span class="hljs-attribute">display</span>: masonry;
}
}
<span class="hljs-keyword">@supports</span> <span class="hljs-keyword">not</span> (<span class="hljs-attribute">display:</span> masonry) {
<span class="hljs-selector-class">.container</span> {
<span class="hljs-attribute">display</span>: grid;
}
}
</code></pre>
<p>I love that CSS is fully embracing the idea of progressive enhancement! I've used feature testing in JS for a long while, but hadn't realized this was now baked in to CSS, too.</p>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-11-03-css-feature-selectors.html">CSS Feature Selectors</a> was originally
published <time class="dt-published" datetime="2025-11-03T14:09:00-06:00">3 November 2025</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
Linux desktop files and xdg-open - Blog entries :: mwop.nethttps://mwop.net/blog/2025-11-03-desktop-xdg-open.html2025-11-03T19:39:35.000Z<p>I've been using Linux on the desktop for more than 25 years now. While I don't put icons on my desktop any longer (and haven't for probably around 15 years), I <em>do</em> use the gnome-shell launcher to quickly open programs, and this utilizes desktop files.</p>
<p>Recently, I wanted to create launchers for different Obsidian vaults. Obsidian provides a URL schema for this: <code>obsidian://open?vault=VaultName</code>. The application registers the schema handler with the system, so this should open, but evidently you can no longer use "Type=Link" in your desktop files.</p>
<p>What I found:</p>
<ul>
<li>You MUST have a "Version=1.0" line; gnome-shell just ignored any of my desktop files that omitted it.</li>
<li>You can use <code>xdg-open</code> in your <code>Exec</code> line to open the URL.</li>
</ul>
<pre><code class="language-ini hljs ini" data-lang="ini"><span class="hljs-section">[Desktop Entry]</span>
<span class="hljs-attr">Version</span>=<span class="hljs-number">1.0</span>
<span class="hljs-attr">Name</span>=Notes
<span class="hljs-attr">Icon</span>=/usr/share/icons/hicolor/<span class="hljs-number">256</span>x256/apps/obsidian.png
<span class="hljs-attr">Comment</span>=My Obsidian vault for notes
<span class="hljs-attr">Categories</span>=<span class="hljs-literal">Off</span>ice<span class="hljs-comment">;ProjectManagement;</span>
<span class="hljs-attr">Type</span>=Application
<span class="hljs-attr">Exec</span>=xdg-open <span class="hljs-string">"obsidian://open?vault=notes"</span>
</code></pre>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-11-03-desktop-xdg-open.html">Linux desktop files and xdg-open</a> was originally
published <time class="dt-published" datetime="2025-11-03T13:39:35-06:00">3 November 2025</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
The State of PHP 2025 - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=6466392025-10-15T14:00:04.000Z
<p><em>The State of PHP 2025 </em>examines how developers use, prefer, and rely on PHP, showing how this long-standing web language continues to modernize through new frameworks, improved tooling, and AI-assisted workflows.</p>
<p>In this report, we present findings from the <a href="https://devecosystem-2025.jetbrains.com/" target="_blank" rel="noopener">Developer Ecosystem Survey 2025</a>. Alongside the numbers, you’ll also hear commentary from <a href="https://x.com/brendt_gd" target="_blank">Brent Roose</a>, JetBrains Developer Advocate for PHP, and insights from other community experts explaining what’s shaping PHP today and where the ecosystem is heading.</p>
<p>If you’d like to see what the ecosystem looked like just a year ago, check out <em>the<a href="https://blog.jetbrains.com/phpstorm/2025/02/state-of-php-2024/"> State of PHP 2024</a></em>.</p>
<h2 class="wp-block-heading">Participants</h2>
<p>This year, we collected responses from 1,720 developers who indicated PHP as their main programming language, with the largest populations living in Japan, the United States, Russia, China, and France.</p>
<figure class="wp-block-image size-full"><img decoding="async" fetchpriority="high" width="1700" height="1896" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/01-2x.png" alt="" class="wp-image-648206"/></figure>
<p>88% of PHP developers have more than three years of experience, with the largest single group falling in the six-to-ten-year range.</p>
<figure class="wp-block-image size-full"><img decoding="async" width="1700" height="1306" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/02-2x.png" alt="" class="wp-image-648217"/></figure>
<h3 class="wp-block-heading">Team size and work environment</h3>
<p>More than half of PHP developers (56%) work in small teams of two to seven people, while 12% work independently.</p>
<figure class="wp-block-image size-full"><img decoding="async" width="1700" height="1236" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/03-2x.png" alt="" class="wp-image-648228"/></figure>
<h2 class="wp-block-heading">Language adoption and usage</h2>
<p>The majority of PHP developers (58%) do not plan to migrate to other languages in the next year. For those who do, Go and Python are the most attractive alternatives.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1404" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/04-2x.png" alt="" class="wp-image-648240"/></figure>
<p>The share of newcomers is slowly growing: 4% have been using PHP for less than six months (up from 2% last year), and 6% for less than one year. Still, almost three-quarters (72%) of developers report over four years of PHP usage, underlining the ecosystem’s maturity.</p>
<div class="blockquote">
<blockquote><p>“Important to note is that these numbers aren’t talking about people leaving PHP – they are about adopting languages besides PHP. I think it’s great to see so many PHP developers who are adding other languages to their toolbelt. PHP has areas where it shines, but there are also problems that are better solved with languages like Go or Rust. Working together across those language barriers leads to great results.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2024/09/brent-roose.jpg" alt="Brent Roose, JetBrains Developer Advocate">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Brent Roose</strong>
<span class="blockquote__author-subtitle">JetBrains Developer Advocate for PHP</span>
</div>
</div>
</div>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1596" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/05-2x.png" alt="" class="wp-image-648251"/></figure>
<div class="blockquote">
<blockquote><p>“It’s great to see more newcomers in PHP. It’s not surprising given the amount of positive buzz around PHP for the past couple of years, but it’s nice to see the numbers proving this trend as well.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2024/09/brent-roose.jpg" alt="Brent Roose, JetBrains Developer Advocate">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Brent Roose</strong>
<span class="blockquote__author-subtitle">JetBrains Developer Advocate for PHP</span>
</div>
</div>
</div>
<p>The trend toward modernization continues: PHP 8.x dominates with 89% usage, while PHP 7.x has dropped to 33%. Legacy versions (5.6 and earlier) are now down to 8%, though not entirely gone. Read <a href="https://stitcher.io/blog/php-version-stats-june-2025" target="_blank" rel="noopener">Brent’s blog post</a> for a more in-depth analysis of PHP versions’ usage.</p>
<div class="blockquote">
<blockquote><p>“I think the open-source community plays a vital role in pushing the PHP community forward to adopt more secure and performant versions. The best part is that by using tools like <a href="https://getrector.com/" target="_blank" rel="noopener">Rector</a>, upgrading becomes almost trivial. I speak from experience: The yearly upgrade is so well worth it.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2024/09/brent-roose.jpg" alt="Brent Roose, JetBrains Developer Advocate">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Brent Roose</strong>
<span class="blockquote__author-subtitle">JetBrains Developer Advocate for PHP</span>
</div>
</div>
</div>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1184" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/06-2x.png" alt="" class="wp-image-648262"/></figure>
<h2 class="wp-block-heading">PHP frameworks and CMSs</h2>
<p>No major shifts occurred in framework adoption: <a href="https://laravel.com/" target="_blank" rel="noopener">Laravel</a> continues to lead with 64% usage, followed by <a href="https://wordpress.org/" target="_blank" rel="noopener">WordPress</a> (25%) and <a href="https://symfony.com/" target="_blank" rel="noopener">Symfony</a> (23%). Other frameworks like CodeIgniter, Yii, and CakePHP hold smaller but stable shares.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1760" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/07-2x.png" alt="" class="wp-image-648194"/></figure>
<div class="blockquote">
<blockquote><p>“We’re thrilled to see Laravel adoption continuing to grow, driven by innovations like <a href="https://cloud.laravel.com/" target="_blank" rel="noopener">Laravel Cloud</a> and AI integrations such as <a href="https://github.com/laravel/boost" target="_blank" rel="noopener">Laravel Boost</a> and MCP. Laravel remains focused on providing a comprehensive, modern full-stack solution that makes PHP development more productive and accessible than ever.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/02/taylor-otwell.png" alt="Taylor Otwell">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Taylor Otwell</strong>
<span class="blockquote__author-subtitle">Creator of Laravel</span>
</div>
</div>
</div>
<div class="blockquote">
<blockquote><p>“Symfony contributors continue to push the boundaries of what can be expressed through type annotations, creating a virtuous cycle where codebases, static analyzers, and IDEs continuously improve to enhance developer experience and verifiability. Informal communication channels between all stakeholders make this progress even smoother and more efficient.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/nicolas-grekas.jpeg" alt="Nicolas Grekas">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Nicolas Grekas</strong>
<span class="blockquote__author-subtitle">Core Developer at Symfony</span>
</div>
</div>
</div>
<h2 class="wp-block-heading">PHP development environments</h2>
<h3 class="wp-block-heading">Most used IDE or editor</h3>
<p>One of the most striking changes is in tooling: The share of those using either <a href="https://www.jetbrains.com/phpstorm/" target="_blank" rel="noopener">PhpStorm</a> or IntelliJ IDEA with the PHP plugin has gone up by 10 percentage points, to 68%. Visual Studio Code’s share has dropped to 23%, while new players like Cursor (6%) have also entered the scene.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1378" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/08-2x.png" alt="" class="wp-image-648183"/></figure>
<h3 class="wp-block-heading">Satisfaction with coding tools</h3>
<p>We also asked PHP developers how satisfied they are with their primary IDE. Among them, 53% of PhpStorm users gave their IDE the highest possible rating, compared to just 26% of VS Code users.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1762" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/09-2x.png" alt="" class="wp-image-648172"/></figure>
<h3 class="wp-block-heading">IDE or editor of choice per framework</h3>
<p>PhpStorm dominates in Symfony (83%) and leads in Laravel (62%), while WordPress developers remain more split, with VS Code remaining a popular choice (37%).</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1788" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/10_2-2x.png" alt="" class="wp-image-648161"/></figure>
<p>Did you know Laravel support is now free for all PhpStorm users? <a href="https://blog.jetbrains.com/phpstorm/2025/07/laravel-idea-is-now-free/">This blog post</a> has the full story.</p>
<h3 class="wp-block-heading">Debugging and testing</h3>
<p>When it comes to debugging, most developers still rely on var_dump-style approaches (59%), though debugger adoption (e.g. <a href="https://xdebug.org/" target="_blank" rel="noopener">Xdebug</a>) has risen slightly to 39%.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1382" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/11-2x.png" alt="" class="wp-image-648149"/></figure>
<div class="blockquote">
<blockquote><p>“Xdebug’s usage seems to be pretty stable throughout the years and across several surveys: between 30% and 35%. While I definitely still do my fair share of “dd” or “log” debugging, there are times where having a debugger at hand and knowing how to use it saves so much time. It’s a skill that takes practicing and doesn’t come overnight – which is why I made this <a href="https://www.jetbrains.com/pages/phpstorm-getting-started/episode-6/" target="_blank" rel="noopener">short video</a> to help folks get started with Xdebug.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2024/09/brent-roose.jpg" alt="Brent Roose, JetBrains Developer Advocate">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Brent Roose</strong>
<span class="blockquote__author-subtitle">JetBrains Developer Advocate for PHP</span>
</div>
</div>
</div>
<p><a href="https://phpunit.de/" target="_blank" rel="noopener">PHPUnit</a> (50%) remains the standard, but <a href="https://pestphp.com/" target="_blank" rel="noopener">Pest</a> adoption has gained four percentage points to reach 17%, showing momentum toward modern, developer-friendly testing frameworks.</p>
<div class="blockquote">
<blockquote><p>“I’m super-happy to see more and more people choosing Pest as their go-to testing framework – the growth in this year’s survey really reflects the quality of our recent releases.<br />
<br />
Since the survey, we’ve actually released Pest 4, which introduces test sharding, profanity checking, and the revolutionary <a href="https://pestphp.com/docs/pest-v4-is-here-now-with-browser-testing" target="_blank" rel="noopener">browser testing</a>. Having proper browser testing in the PHP world is truly a game-changer, so I expect adoption to increase even more next year!”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/02/nuno-maduro.jpg" alt="Nuno Maduro">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Nuno Maduro</strong>
<span class="blockquote__author-subtitle">Creator of Pest</span>
</div>
</div>
</div>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1514" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/12-2x.png" alt="" class="wp-image-648133"/></figure>
<p>However, 32% of developers still don’t write tests at all, which highlights a persistent gap in testing culture.</p>
<h3 class="wp-block-heading">Code quality tools</h3>
<p>The clear winner in 2025 is <a href="https://phpstan.org/" target="_blank" rel="noopener">PHPStan</a>, which jumped to 36% usage, up nine percentage points from last year. Tools like <a href="https://github.com/PHP-CS-Fixer/PHP-CS-Fixer" target="_blank" rel="noopener">PHP CS Fixer</a> (30%) and <a href="https://github.com/squizlabs/PHP_CodeSniffer" target="_blank" rel="noopener">PHP_CodeSniffer</a> (22%) remain widely used, while <a href="https://github.com/rectorphp/rector" target="_blank" rel="noopener">Rector</a> (10%) continues its steady rise. Still, 42% of respondents don’t use any code quality tools regularly, leaving room for further improvement.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1596" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/13-2x.png" alt="" class="wp-image-648122"/></figure>
<figure class="wp-block-table"><table><tbody><tr><td>💡Have you tried <a href="https://www.jetbrains.com/qodana/" target="_blank" rel="noopener">JetBrains Qodana</a>? This static analysis and codebase auditing tool brings inspections from PhpStorm into your CI/CD pipeline, along with unique and custom code-quality and security checks. Use it to clean up and secure your team’s code before merging to the main branch.</td></tr></tbody></table></figure>
<h2 class="wp-block-heading">Adoption of AI</h2>
<p>AI has gone mainstream: 95% of developers have tried at least one AI tool, and 80% regularly use AI assistants or AI-powered editors.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1378" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/14-2x.png" alt="" class="wp-image-648111"/></figure>
<p><a href="https://chatgpt.com/" target="_blank" rel="noopener">ChatGPT</a> leads daily use with 49%, though its share has dropped since 2024. <a href="https://github.com/features/copilot" target="_blank" rel="noopener">GitHub Copilot</a> (29%) and <a href="https://www.jetbrains.com/ai-assistant/" target="_blank" rel="noopener">JetBrains AI Assistant</a> (20%) follow, with the latter tripling its adoption since last year.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="2234" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/15-2x.png" alt="" class="wp-image-648100"/></figure>
<div class="blockquote">
<blockquote><p>“AI is here to stay. It’s great to see the increased adoption of AI overall, and the drop in ChatGPT usage in favor of more specialized tooling. We can expect more of this moving forward, with new tools entering the scene, and the ones giving us the best boost earning their place in our daily stack.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/ashley-laravel.jpg" alt="">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Ashley Hindle</strong>
<span class="blockquote__author-subtitle">AI Engineer at Laravel</span>
</div>
</div>
</div>
<p>Looking ahead, 72% of respondents are likely to try AI coding agents in the next year, while only 8% say it’s unlikely.</p>
<figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1700" height="1214" src="https://blog.jetbrains.com/wp-content/uploads/2025/10/16-2x.png" alt="" class="wp-image-648072"/></figure>
<p>To support developers in this shift, earlier this year JetBrains introduced an AI coding agent called <a href="https://www.jetbrains.com/junie/" target="_blank" rel="noopener">Junie</a>. Unlike other coding assistants, Junie is built to work directly with your JetBrains IDE, project context, and team practices, delivering actionable and trustworthy support. With AI agents rapidly becoming part of everyday workflows, Junie brings the reliability, integration, and developer focus you expect from JetBrains to this new wave of tools.</p>
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Meet Junie: Your Coding Agent in PyCharm, a JetBrains IDE" width="500" height="281" src="https://www.youtube.com/embed/wpz_0MgNZ5w?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>
<p>At the same time, not every company is ready to embrace AI yet. 11% respondents report that their organization is unlikely to try AI coding agents in the next 12 months. Apart from the data privacy and security concerns (44%) and intellectual property questions (24%), many companies also struggle with a lack of knowledge about such tools (22%).</p>
<h2 class="wp-block-heading">Ecosystem highlights 2025</h2>
<h3 class="wp-block-heading">FrankenPHP</h3>
<div class="blockquote">
<blockquote><p>“One of my personal highlights in PHP this year is <a href="https://frankenphp.dev/" target="_blank" rel="noopener">FrankenPHP</a> becoming a <a href="https://thephp.foundation/blog/2025/05/15/frankenphp/" target="_blank" rel="noopener">project backed by the PHP Foundation</a>. I think there’s a lot of potential for the project to become the de-facto standard runtime for PHP, which would be huge. FrankenPHP has a lot of performance optimizations that work out of the box for any PHP application, it’s portable across systems, and it has worker mode, which allows for asynchronous request handling in PHP, which can speed up applications by a factor of three compared to using PHP FPM.”</p></blockquote>
<div class="blockquote__author">
<img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2024/09/brent-roose.jpg" alt="Brent Roose, JetBrains Developer Advocate">
<div class="blockquote__author-info">
<strong class="blockquote__author-title">Brent Roose</strong>
<span class="blockquote__author-subtitle">JetBrains Developer Advocate for PHP</span>
</div>
</div>
</div>
<p>Learn more about FrankenPHP from Kévin Dunglas on PHPverse 2025:</p>
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="FrankenPHP: the future of PHP? // PHPverse 2025" width="500" height="281" src="https://www.youtube.com/embed/k-UwH91XnAo?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>
<h3 class="wp-block-heading">PHPverse</h3>
<p>This year also marked a special milestone – PHP turned 30 years old. We celebrated with <a href="https://lp.jetbrains.com/phpverse-2025/" target="_blank" rel="noopener">JetBrains PHPverse</a>, an online birthday event that drew <strong>more than 26,000 viewers worldwide</strong>. If you missed it, don’t worry – the recordings are available to watch on demand:</p>
<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="JetBrains PHPverse 2025 – Join us to celebrate PHP’s 30th birthday!" width="500" height="281" src="https://www.youtube.com/embed/3b0ty1iZ8QM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>
<p>Catch the next edition of PHPverse – sign up here to be the first to know when registration opens.</p>
<div class="buttons">
<div class="buttons__row">
<a href="https://lp.jetbrains.com/phpverse-2025/#dont-miss-the-next-phpverse" class="btn" target="" rel="noopener">Get event updates</a>
</div>
</div>
<h2 class="wp-block-heading">What this means for PHP</h2>
<p>The 2025 results confirm that PHP remains a stable, professional, and evolving ecosystem. Its strong developer base, continued dominance of Laravel and WordPress, increasing adoption of modern tooling, and rapid embrace of AI-powered workflows show that PHP is far from being “legacy”.</p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<p><em>Disclaimer: Despite all the measures we’ve taken to secure a representative pool of respondents, these results might be slightly skewed toward users of JetBrains products, as they might have been more likely to take the survey. </em><a href="https://lp.jetbrains.com/developer-ecosystem-2025-methedology/" target="_blank" rel="noopener"><em>Read more</em></a><em> about our methodology.</em></p>
Appending text to Obsidan via Shortcuts - Rob Allenhttps://akrabat.com/?p=75162025-10-14T10:00:00.000Z<p>Sometimes it's helpful to add some text to my current Obsidian daily note without having to switch to Obsidian, find the daily note and then type my text.</p>
<p>To do this, we can use the magic of Obsidian's <a href="https://help.obsidian.md/Extending+Obsidian/Obsidian+URI#Create%20note"><tt>obsidian://</tt> URI schema</a> and automate the text capture in Apple Shortcuts, with an assigned keyboard shortcut to activate it.</p>
<p>This is the Shortcut:</p>
<picture><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025add-to-daily-note-obsidian-shortcut-dark.png"
media="(prefers-color-scheme: dark)"
/><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025add-to-daily-note-obsidian-shortcut-light.png"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/><br />
<img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/09/2025add-to-daily-note-obsidian-shortcut-light.png" loading="lazy" alt="Add to daily note obsidian shortcut light." class="no-border" width="600"/>
</picture>
<p>There's a number of steps in the Shortcut. Firstly we <em>Ask For</em> some text which will display a dialog box for us to type in. Then we <em>Replace</em> any trailing whitespace, add then use a <em>Text action</em> to prepend the current time as a third level heading. This is the text to be added, so we <em>URL Encode</em> it and use another <em>Text action</em> to create the full <tt>obsidian://</tt> URI to be <em>Open</em>ed.</p>
<p>The URI is:</p>
<pre>
obsidian://daily?vault=General&append=1&content={URL Encoded Text}
</pre>
<p>The <tt>daily</tt> endpoint only works if the <a href="https://help.obsidian.md/plugins/daily-notes">Daily notes</a> core plugin is enabled. We then need to specify which <tt>vault</tt> we're using. The <tt>content</tt> is the URL encoded text to be appended and the <tt>append</tt> parameter tells obsidian to place the text at the end of the note.</p>
<p>I've assigned it to <tt style="font-family: sans-serif">⌃⌥⌘=</tt> and pressing that key combination runs it for me.</p>
<p>Now I have a simple way to add some text to the current note without interrupting my context.</p>
<p>If this shortcut would be useful to you, you can <a href="https://www.icloud.com/shortcuts/1e8f12d16e8049a684f3282d950e6352">download it here</a>.</p>
mac-volume: A CLI to control device volume - Rob Allenhttps://akrabat.com/?p=75122025-10-07T10:00:00.000Z<p>I've set my Mac up such that video calls such as Zoom use the microphone and earphones attached to my <a href="https://www.behringer.com/product.html?modelCode=0805-AAS">Behringer UMC204HD</a>, which all other audio plays through the my normal speakers which are the default.</p>
<p>One issue I have with this is that it's quite hard to change the volume when a call as the volume buttons on the Mac are connected to the default output. This finally annoyed me enough that I looked into how to control the volume of a device and wrote a little command line utility for this, so that I could attach it to buttons on my <a href="https://akrabat.com/controlling-the-streamdeck-via-keyboard-maestro/">StreamDeck using Keyboard Maestro</a>.</p>
<h2>Changing the volume</h2>
<p>The way to control the volume on a Mac programmatically is to use Core Audio, specifically <a href="https://developer.apple.com/documentation/coreaudio/audioobjectsetpropertydata(_:_:_:_:_:_:)?language=objc"><tt>AudioObjectSetPropertyData()</tt></a>. This function takes a <tt>deviceID</tt> and some other properties, including <tt>volume</tt>. The <tt>deviceID</tt> is of type <a href="https://developer.apple.com/documentation/coreaudio/audiodeviceid"><tt>AudioDeviceID</tt></a> and the <tt>volume</tt> is a floating point number between <tt>0</tt> and <tt>1</tt>.</p>
<p>It's not easy to remember device IDs, so I decided to use the device name instead and map the volume to between 0 and 100. This makes it easy to say set the volume to 10%:</p>
<pre>
mac-volume "MacBook Pro Speakers" set 10
</pre>
<p>To get the list of device names, I wrote a <tt>list-devices</tt> command. The list is available from <tt>AudioObjectGetPropertyData()</tt> which returns list of <tt>AudioDeviceID</tt> items. The name is in the <tt>kAudioObjectPropertyName</tt> selector, so that's easy enough.</p>
<p>The main trouble is that CoreAudio is a C API and so dealing with it from Swift involves use of references and <a href="https://developer.apple.com/documentation/swift/unsafemutablepointer">UnsafeMutablePointer</a> which is a bit hairy as it's been a while since I've had to worry about pointers!</p>
<h3>Increment and decrement</h3>
<p>While you can get the current volume, add to it and then set to increase the volume, it's easier to let the app do it, so <tt>mac-volume</tt> also supports incrementing and decrementing by an amount. These are the two command that I bind to StreamDeck keys.</p>
<picture><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-dark.png"
media="(prefers-color-scheme: dark)"
/><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-light.png"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/><br />
<img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/09/2025mac-vol-down-light.png" loading="lazy" alt="Keyboard Maestro configuration for mac-volume down" class="border" width="550"/>
</picture>
<p>I found that a step size of 3 was a good choice for raising/lowering the volume with a reasonable number of presses of the key.</p>
<h3>It's on GitHub</h3>
<p>If you need to control the volume of a non-default audio device from the command line, <tt>mac-volume</tt> is on GitHub at <a href="https://github.com/akrabat/mac-volume">akrabat/mac-volume</a>.</p>
Moving PHP open source forward - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=6456022025-10-02T09:10:34.000Z
<p>At JetBrains, we want the PHP community to shine. First and foremost, we do this by building <a href="https://www.jetbrains.com/phpstorm/" target="_blank" rel="noopener">PhpStorm</a>, the best IDE for PHP development; but we also <a href="https://blog.jetbrains.com/phpstorm/2021/11/the-php-foundation/">support and help drive the PHP Foundation</a>; have just <a href="https://www.youtube.com/playlist?list=PL0bgkxUS9EaI6rjDdRvihJn90OztDsxTO" target="_blank" rel="noopener">organised PHPverse</a>, accessible for everyone worldwide; and recently we made the <a href="https://blog.jetbrains.com/phpstorm/2025/07/laravel-idea-is-now-free/">Laravel Idea plugin free for all</a>.</p>
<p>On top of that, we <a href="https://www.jetbrains.com/community/opensource/" target="_blank" rel="noopener">support open-source projects</a> that we believe impact the PHP community or have the potential to do so, and which will benefit from our financial support. </p>
<p>Our support includes <strong>free PhpStorm licenses</strong> for active open source maintainers, but also <strong>financial sponsorships</strong>. With this blog post, we’d like to announce a new round of sponsorships, as well as a more structured approach towards new open source sponsorships in the future.</p>
<h2 class="wp-block-heading">Sponsorships 2025-2026</h2>
<p>Our strategy from now on is to sponsor around five open-source projects and maintainers per year. Each year we’ll select new projects to diversify our support.</p>
<p>Here are the people we’ll sponsor for the coming year:</p>
<ul>
<li><a href="https://github.com/azjezz" target="_blank" rel="noopener">Saif Eddin Gmati</a>, who’s building a very promising new linter and static analyzer for PHP in Rust: <a href="https://github.com/carthage-software/mago" target="_blank" rel="noopener">Mago</a>.</li>
<li><a href="https://github.com/sponsors/staabm" target="_blank" rel="noopener">Markus Staab</a>, who’s involved in tons of existing open-source projects like PHPStan, Rector, and PHPUnit.</li>
<li><a href="https://github.com/CodeWithKyrian" target="_blank" rel="noopener">Kyrian Obikwelu</a>, who’s actively exploring AI and MCP possibilities in PHP.</li>
<li><a href="https://github.com/SjonHortensius" target="_blank" rel="noopener">Sjon Hortensius</a>, who’s responsible for <a href="http://3v4l.org" target="_blank" rel="noopener">3v4l.org</a>, an online shell for PHP that’s very popular within the PHP community.</li>
</ul>
<p>You may have noticed there’s still one spot open. Maybe you know just the right person or project to add to this list? Feel free to reach out to <a href="mailto:brent.roose@jetbrains.com">brent.roose@jetbrains.com</a> with your suggestions, or leave a comment below this blog post!</p>
<p>Apart from sponsoring individual projects, we also stay committed to sponsoring the <a href="https://thephp.foundation/" target="_blank" rel="noopener">PHP Foundation</a>, and encourage as many organizations to do so as well.</p>
<div class="buttons">
<div class="buttons__row">
<a href="https://thephp.foundation/sponsor/" class="btn" target="" rel="noopener">Sponsor PHP Foundation</a>
</div>
</div>
<p>Finally, because of this new yearly system, we also stopped sponsoring two projects we’ve supported for many years. Not because we don’t believe in them anymore, but because we want to make sure we can support as many different projects as possible over time. Maybe this is a good time to take a look if you or your company can pick up new sponsorships as well?</p>
<ul>
<li><a href="https://github.com/sponsors/derickr" target="_blank" rel="noopener">Derick Rethans</a>, who’s working on Xdebug</li>
<li><a href="https://github.com/sponsors/jrfnl" target="_blank" rel="noopener">Juliette Reinders Folmer</a>, who’s working on CodeSniffer</li>
</ul>
<p>We believe in the power of open-source. We want to support it and encourage you to do the same if you have the means to do so. </p>
<p>Let’s work together to make PHP even better!</p>
Opening Slack Jira Cloud links in the right browser - Rob Allenhttps://akrabat.com/?p=74992025-09-30T10:00:00.000Z<p>I use <a href="https://loshadki.app/openin4/">OpenIn</a> to open links in a given browser when I click on them in other applications. This is really helpful to keep various work related stuff in different browsers or profiles and I find it very helpful.</p>
<p>One thing that's bothered me is that links from the <a href="https://slack.com/marketplace/A2RPP3NFR-jira-cloud">Jira Cloud Slack App</a> ignore my OpenIn rules and always open in Safari and I finally sat down to work out why.</p>
<h2>The investigation</h2>
<p>I create a new OpenIn rule and enabled multiple browsers so that OpenIn would present a choice window to me.</p>
<p>It looks like this</p>
<picture><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-dark.png"
media="(prefers-color-scheme: dark)"
/><source
srcset="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-light.png"
media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)"
/><br />
<img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/09/2025slack-app-linkopen-in-choices-light.png" loading="lazy" alt="Slack app linkopen in choices light." class="no-border" width="266"/>
</picture>
<p>At the bottom, we can see the link that OpenIn has received.</p>
<p>Even though the presented URL in the Slack app is <tt>https://my-client.atlassian.net/browser/PROJ-123</tt>, and if you right click and copy link, that's what you get in your clipboard, when you <em>click</em> on the link, you get a slack.com link.</p>
<p>Copying that link, it's of the form: <tt>https://slack.com/openid/connect/login_initiate_redirect?login_hint=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.blah.blah</tt>. </p>
<p>That's interesting! I've seen that <tt>eyJhbGci</tt> prefix often enough to know on sight that it's the start of base64 encoded <a href="https://www.rfc-editor.org/rfc/rfc7519">JWT</a> and sure enough, base64 decoding it proved that it was.</p>
<p>The format of JWT is <tt>{header}.{payload}.{signature}</tt> and it's unencrypted, so we can inspect the payload easily enough. This is redacted, but it's of the form:</p>
<pre>
{
"aud" : "123456789.123456789",
"auth_time" : 1758615553,
"exp" : 1758619153,
"https://slack.com/target_uri" : "https://{my-client}.atlassian.net/browse/PROJ-123",
"https://slack.com/team_id" : "ABCDEFGHI",
"https://slack.com/user_id" : "U01AB2CDEF3",
"iat" : 1758615553,
"iss" : "https://slack.com",
"sub" : "rob@{my-client}.com"
}
</pre>
<p>We can see that the URL that we want to go to is in the payload's <tt>"https://slack.com/target_uri"</tt> property, so we <em>just</em> need to set up OpenIn to use that URL and pick the appropriate browser.</p>
<h2>OpenIn's custom scripts</h2>
<p>One really nice feature of OpenIn is that you can write <a href="https://loshadki.app/openin4/080-javascript-for-rule/">custom scripts for a rule</a>, so we can use this to extract the payload and pick the browser.</p>
<p>So I read the web page and wrote some Javascript!</p>
<p>The work we need to do is:</p>
<ol>
<li>Extract the payload from the query parameter</li>
<li>JSON decode it</li>
<li>Read the target_uri</li>
<li>Choose browser based on target_uri</li>
</ol>
<p>This needs a few helper functions</p>
<h3>Helper functions</h3>
<p>Extracting the payload is straightforward JS. We need the text between the two <tt>.</tt>s:</p>
<pre lang="javascript">
function extractJwtPayload(str) {
const parts = str.split('.');
return parts.length >= 3 ? parts[1] : '';
}
</pre>
<p>As OpenIn uses WebKit's <a href="https://docs.webkit.org/Deep%20Dive/JSC/JavaScriptCore.html">JavaScriptCore</a>, we can decode base64 using <tt>Uint8Array.fromBase64()</tt>:</p>
<pre lang="javascript">
function base64Decode(str) {
const uint8Array = Uint8Array.fromBase64(str);
return String.fromCharCode(...uint8Array);
}
</pre>
<p>We also need to select the browser that we want the link to open in. This is done in OpenIn by setting all the "visible" browsers to false, except the one you want:</p>
<pre lang="javascript">
function selectBrowser(browser) {
let apps = ctx.getApps()
apps.forEach(function (app) {
app.visible = (app.name == browser)
})
}
</pre>
<h3>Do the work</h3>
<p>Having set everything up, we can now find the <tt>target_uri</tt> and choose the browser. </p>
<p>Firstly we only want to do this work if the source app is Slack and that we have a <tt>login_hint</tt> parameter:</p>
<pre lang="javascript">
if (ctx.getSourceApp().path.startsWith("/Applications/Slack.app")
&& ctx.url.searchParams.has('login_hint')) {
</pre>
<p>Extracting the <tt>target_uri</tt> is a case of using the functions we've written:</p>
<pre lang="javascript">
const login_hint = ctx.url.searchParams.get('login_hint');
const jwtPayload = extractJwtPayload(login_hint);
const payload = base64Decode(jwtPayload).replace(/\0/g, '');
const data = JSON.parse(payload);
const target_uri = data['https://slack.com/target_uri'];
</pre>
<p>Note that I discovered some null bytes during testing, so removed them from the decoded string.</p>
<p>Now we set OpenIn's URI and select the browser we want:</p>
<pre>
// Set OpenIn's URL
ctx.url.href=target_uri;
// Select the browser
if (target_uri.includes("my-client-1")) {
selectBrowser("Firefox");
return;
}
if (target_uri.includes("my-client-2")) {
selectBrowser("Firefox");
return;
}
// Default to Safari
selectBrowser("Safari");
return;
</pre>
<p>and we're done.</p>
<h2>That's it</h2>
<p>That's it! Whenever I click on a Jira link in Slack, the correct browser opens directly to where I want to go.</p>
<p>The full script is here: <a href="https://akrabat.com/wp-content/uploads/2025-09-openin-slack-app-link.js">openin-slack-app-link.js</a></p>
Disabling the Zoom mini window on Linux - Blog entries :: mwop.nethttps://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html2025-09-23T14:20:27.000Z<p>Zoom used to have a strange behavior, one I'm sure they thought would be useful, but in reality was infuriating: if you moved between virtual workspaces, Zoom would minimize to a thumbnail window that followed you around to workspaces.</p>
<p>At some point, it went away, thankfully... but after a recent release, it turned back on, and it's been a huge pain for me. There's a bug in that the mini window follows me to the initial virtual workspace, but then doesn't follow around from there, requiring me to use the workspace tools to move it to the workspace I want, and then re-maximize it, only to have to do the whole thing again if I switch screens.</p>
<p>The solution turns out to be relatively simple, if incredibly unintuitive.</p>
<p>Zoom does NOT have a GUI setting for this, for some reason. But evidently it creates the file <code>$HOME/.config/zoomus.conf</code>, which is where all configuration is stored.</p>
<p>This file is in <a href="https://en.wikipedia.org/wiki/INI_file">INI format</a>. Find the entry <code>enableMiniWindow</code>, and set it to false:</p>
<pre><code class="language-dosini">enableMiniWindow=false
</code></pre>
<p>Restart Zoom, and you're set.</p>
<div class="h-entry">
<img class="u-photo photo" width="50" src="https://avatars0.githubusercontent.com/u/25943?v=3&u=79dd2ea1d4d8855944715d09ee4c86215027fa80&s=140" alt="matthew">
<a class="u-url u-uid p-name" href="https://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html">Disabling the Zoom mini window on Linux</a> was originally
published <time class="dt-published" datetime="2025-09-23T09:20:27-05:00">23 September 2025</time>
on <a href="https://mwop.net">https://mwop.net</a> by
<a rel="author" class="p-author" href="https://mwop.net">Matthew Weier O'Phinney</a>.
</div>
Fixing PostgreSQL collation version mismatch - Rob Allenhttps://akrabat.com/?p=74932025-09-23T10:00:00.000Z<p>After pulling a new version of the Docker PostgreSQL container, I started getting this warning:</p>
<pre>
WARNING: database "dev" has a collation version mismatch
DETAIL: The database was created using collation version 2.36, but the operating system provides version 2.41.
HINT: Rebuild all objects in this database that use the default collation and run ALTER DATABASE dev REFRESH COLLATION VERSION, or build PostgreSQL with the right library version.
</pre>
<p>This seems to have occurred because the underlying OS was changed to Trixie from Bullseye for the default image. i.e. I used:</p>
<pre>
image: postgres:17
</pre>
<p>Rather than:</p>
<pre>
image: postgres:17-bookworm
</pre>
<p>This is on my dev machine, so it's not a major problem as usually I can just blow away the database and recreate it. However, I have some test data that I don't want to lose quite yet, so I took a backup using <tt>pg_dump</tt> and then did this instead:</p>
<pre>
REINDEX DATABASE dev;
ALTER DATABASE dev REFRESH COLLATION VERSION;
</pre>
<p>The warning's now gone. </p>
<p>Of course, in production, you'll want to be a bit more careful and a <tt>pg_dump</tt> and load is more likely to avoid any risks of corruption. As ever, YMMV.</p>
Jumping to the end of bash's history - Rob Allenhttps://akrabat.com/?p=74902025-09-16T10:00:00.000Z<p>I use bash's history all the time, via <tt>ctrl+r</tt> and also with the <a href="https://akrabat.com/context-specific-history-at-the-bash-prompt/"><tt>up and down keys</tt></a>; it's wonderful.</p>
<p>Sometimes, I want to get back to the end of my history and I recently discovered that there's a shortcut for this: <tt>meta+></tt>. It doesn't matter where you are in your history, pressing <tt>meta+></tt> jumps you to the end and you have a blank prompt again.</p>
<p>I use <a href="https://iterm2.com">iTerm2</a> on my Mac and have my right hand <tt>option</tt> key set to <tt>meta</tt>. This is done in Settings→Profiles→Keys, setting "Right Option (C) key:" to "Esc+".</p>
<p>However, to press <tt>meta+></tt>, I need to do <tt>right-option+shift+.</tt> which isn't as easy as <tt>right-option+.</tt>, so let's rebind!</p>
<p>To rebind, I looked up the bash command for this functionality (`end-of-history`), and then added this to my <tt>.bashrc</tt>:</p>
<pre>
bind '"\e.": end-of-history'
</pre>
<p>All done. Now I just press <tt>right-option+.</tt> and I'm back at the end of history as if I'd never navigated it.</p>
Understanding All Relations Between Classes, Interfaces, Traits, and Enums in PHP - Exakathttps://www.exakat.io/?p=159832025-09-15T20:29:11.000Z<h1>Understanding Re<a href="https://www.exakat.io/wp-content/uploads/2025/09/stairs.320.jpg"><img fetchpriority="high" decoding="async" class="alignleft wp-image-15984 size-medium" src="https://www.exakat.io/wp-content/uploads/2025/09/stairs.320-300x300.jpg" alt="All relations between classes in PHP" width="300" height="300" srcset="https://www.exakat.io/wp-content/uploads/2025/09/stairs.320-150x150@2x.jpg 300w, https://www.exakat.io/wp-content/uploads/2025/09/stairs.320-150x150.jpg 150w, https://www.exakat.io/wp-content/uploads/2025/09/stairs.320-100x100.jpg 100w, https://www.exakat.io/wp-content/uploads/2025/09/stairs.320.jpg 320w" sizes="(max-width: 300px) 100vw, 300px" /></a>lationships Between Classes, Interfaces, Traits, and Enums in PHP</h1>
<p data-start="190" data-end="424">Modern PHP offers four major code structures — <strong data-start="237" data-end="248">classes</strong>, <strong data-start="250" data-end="264">interfaces</strong>, <strong data-start="266" data-end="276">traits</strong>, and <strong data-start="282" data-end="291">enums</strong> — each with its own purpose and rules. But the real power of PHP comes from how these structures can <strong data-start="393" data-end="421">interact with each other</strong>. This page gathers all relations between classes in PHP.</p>
<p data-start="426" data-end="683">If you’ve ever wondered <em data-start="450" data-end="478">“Can an enum use a trait?”</em> or <em data-start="482" data-end="521">“Can a trait implement an interface?”</em>, the answer lies in PHP’s keywords: <code data-start="558" data-end="567">extends</code>, <code data-start="569" data-end="581">implements</code>, and <code data-start="587" data-end="592">use</code>. These keywords define the legal relationships between the building blocks of your code.</p>
<p data-start="685" data-end="795">To make this crystal clear, here’s a matrix showing <strong data-start="737" data-end="767">who can interact with whom</strong> — and with which keyword.</p>
<p data-start="685" data-end="795">
<p data-start="685" data-end="795">
<p data-start="685" data-end="795">
<p data-start="685" data-end="795">
<table>
<thead>
<tr>
<th>Can interact with…</th>
<th><strong>class</strong></th>
<th><strong>interface</strong></th>
<th><strong>trait</strong></th>
<th><strong>enum</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/class.ini.html"><strong>class</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/implements.ini.html"><strong>extends</strong></a> one <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/class" class="broken_link"><strong>class</strong></a>(single inheritance); Or <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/composition.ini.html"><strong>composition</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extends</strong></a> one or multiple <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/interface.ini.html"><strong>interface</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/use.ini.html"><strong>use</strong></a> one or multiple <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html"><strong>traits</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> an <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/enum.ini.html"><strong>enum</strong></a> (no inheritance), can only call its cases</td>
</tr>
<tr>
<td><a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/interface.ini.html"><strong>interface</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> a <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/class.ini.html"><strong>class</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extends</strong></a> one or multiple <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/interface.ini.html"><strong>interfaces</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/use.ini.html"><strong>use</strong></a> a <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html"><strong>trait</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/implements.ini.html"><strong>implement</strong></a> or <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> an <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/enum.ini.html"><strong>enum</strong></a></td>
</tr>
<tr>
<td><a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html"><strong>trait</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> a <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/class.ini.html"><strong>class</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/implements.ini.html"><strong>implement</strong></a> an <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/interface.ini.html"><strong>interface</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/use.ini.html"><strong>use</strong></a>other <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html"><strong>traits</strong></a>inside a <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html">trait</a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/use.ini.html"><strong>use</strong></a> an <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/enum.ini.html"><strong>enum</strong></a></td>
</tr>
<tr>
<td><a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/enum.ini.html"><strong>enum</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> a <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/class.ini.html"><strong>class</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/implements.ini.html"><strong>implements</strong></a>one or multiple <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/interface.ini.html"><strong>interfaces</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/use.ini.html"><strong>use</strong></a> one or multiple <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/trait.ini.html"><strong>traits</strong></a></td>
<td><img src="https://s.w.org/images/core/emoji/16.0.1/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cannot <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/extends.ini.html"><strong>extend</strong></a> another <a href="https://php-dictionary.readthedocs.io/en/latest/dictionary/enum.ini.html"><strong>enum</strong></a></td>
</tr>
</tbody>
</table>
<h2 data-start="2102" data-end="2121">Key Takeaways</h2>
<ul data-start="2122" data-end="2590">
<li data-start="2122" data-end="2224">
<p data-start="2124" data-end="2224"><strong data-start="2124" data-end="2135">Classes</strong> are the most versatile: they can extend a class, implement interfaces, and use traits.</p>
</li>
<li data-start="2225" data-end="2292">
<p data-start="2227" data-end="2292"><strong data-start="2227" data-end="2241">Interfaces</strong> can only extend other interfaces — nothing else.</p>
</li>
<li data-start="2293" data-end="2430">
<p data-start="2295" data-end="2430"><strong data-start="2295" data-end="2305">Traits</strong> are like reusable code fragments. They can be used in classes and enums and even use other traits, but cannot stand alone.</p>
</li>
<li data-start="2431" data-end="2590">
<p data-start="2433" data-end="2590"><strong data-start="2433" data-end="2442">Enums</strong> (introduced in PHP 8.1) behave like special classes: they cannot extend other classes or enums, but they can implement interfaces and use traits.</p>
</li>
</ul>
<p>The post <a href="https://www.exakat.io/understanding-all-relations-between-classes-interfaces-traits-and-enums-in-php/">Understanding All Relations Between Classes, Interfaces, Traits, and Enums in PHP</a> appeared first on <a href="https://www.exakat.io">Exakat</a>.</p>
Converting JWKS JSON to PEM using Python - Rob Allenhttps://akrabat.com/?p=74842025-09-09T10:00:00.000Z<p>Following on from my <a href="https://akrabat.com/creating-jwks-json-file-in-php/">earlier exploration of JWKS</a> (<a href="https://www.rfc-editor.org/rfc/rfc7517">RFC7517</a>), I found myself needing to convert the JWKS into PEM format.</p>
<p>This time I turned to Python with my preference of using <a href="https://github.com/astral-sh/uv">uv</a> with <a href="https://akrabat.com/defining-python-dependencies-at-the-top-of-the-file/">inline script metadata</a> and created <a href="#script"><tt>jwks-to-pem.py</tt></a>.</p>
<p>The really nice thing about inline script metadata is that we can use the <a href="https://pypi.org/project/cryptography/">cryptography package</a> to do all the hard work with RSA and serialisation. We just have to remember that the base64 encoded values are <a href="https://datatracker.ietf.org/doc/html/rfc4648#section-5">base64 URL encoded</a> and account for it.</p>
<p>As a single file python script, I make it executable with <tt>chmod +x jwks-to-pem.py</tt> and made it so that I can pipe the output of a <a href="https://curl.se">curl</a> call to it, or pass in a JSON file. I prefer to use the <tt>curl</tt> solution though with:</p>
<pre>
curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
</pre>
<h3>Example</h3>
<p>Here's an example from the <a href="https://developer.api.apps.cam.ac.uk/docs/oauth2/1/routes/.well-known/jwks.json/get">University of Cambridge</a>.</p>
<p>On the day I wrote this article, the JWKS looks like this:</p>
<pre>
$ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json
{
"keys": [
{
"alg": "RS256",
"e": "AQAB",
"n": "6kKjjctVPalX0ypJ2irwog8xIXS9JTABqrSnK_n3YJ4q0aH2-1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxlNJ-G8hfc39jrb_KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5_PB7xwKail5VWOcY0SypIYCPD6Ct5DGnQ_XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSFfGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY-9Jx0zua7ZrjO5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4_XM2sAMQwqJnPBss0U9WwDERk17FMHvb_FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInYEgMhKi3SAML_4ke3dbbG-mjchXPN9OqNd4fydnQIP39WFHmFNk_nIlqvYnALI4xPE-w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W-agC6ITe3wYvKH7SHVp6MYQWVD_0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL_f1ETvzDoewzXLOC8GGu2LZDwDbP0ea6DchReWjZfj4nJx23uQyGAj1h_uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzsbtdec9BPPBVeXjI--c0AWU8",
"kid": "70e0ed3c",
"kty": "RSA",
"use": "sig"
}
]
}
</pre>
<p>They very kindly pretty-print it too!</p>
<p>We can then get the PEM version by piping to <tt>jwks-to-pem.py</tt>:</p>
<pre>
$ curl -s https://api.apps.cam.ac.uk/oauth2/v1/.well-known/jwks.json | jwks-to-pem.py
# Key 0 (kid: 70e0ed3c)
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6kKjjctVPalX0ypJ2irw
og8xIXS9JTABqrSnK/n3YJ4q0aH2+1bjGbWz8p1CaCUqDxQSDuqzvOgMdNGvZrxl
NJ+G8hfc39jrb/KnB0T3ZsuxFz6X0mDzmHhdiPjSDK3M0syC4qg5/PB7xwKail5V
WOcY0SypIYCPD6Ct5DGnQ/XONGXIVG7eaJAHdxJp2BOz0n3BVEFnZUgM5JcfGrSF
fGqb0ZotX2AblwjZKQc58E0EVVykJw8gxW1Bob8rbaVXlMHssfY+9Jx0zua7ZrjO
5C4OMmt9J6zYbVnGVwf62ehGtcLSP6iCG4/XM2sAMQwqJnPBss0U9WwDERk17FMH
vb/FBwxAFxRygd0DclWmQmCYr5uFYck57KGARtyoxrNNAf4AFUHuObjbV24TyInY
EgMhKi3SAML/4ke3dbbG+mjchXPN9OqNd4fydnQIP39WFHmFNk/nIlqvYnALI4xP
E+w09T9jCvjU8hYHHlVMRvRluBnUzJkFnxLse5W+agC6ITe3wYvKH7SHVp6MYQWV
D/0I2rCLV4gqjSpXzKIMs5eejjTQQq0VYumgL/f1ETvzDoewzXLOC8GGu2LZDwDb
P0ea6DchReWjZfj4nJx23uQyGAj1h/uPI1jCd9oeJhbN8jFz2ltYgXYBp51qdSzs
btdec9BPPBVeXjI++c0AWU8CAwEAAQ==
-----END PUBLIC KEY-----
</pre>
<h3>The script</h3>
<p>This is the script in case anyone else finds it useful:</p>
<pre lang="python" id="script">
#!/usr/bin/env -S uv run --script --quiet
# /// script
# dependencies = [
# "cryptography",
# ]
# ///
"""Convert JWK keys to PEM format.
This script reads .well-known/jwks.json and outputs PEM encoded versions
of the public keys in that file.
Usage:
curl -s https://example.com/.well-known/jwks.json | jwks-to-pem.py
uv run jwks-to-pem.py jwks.json
uv run jwks-to-pem.py < jwks.json
Requirements:
- uv (https://github.com/astral-sh/uv)
- cryptography library
Author:
Rob Allen <rob@akrabat.com>
Copyright 2025
License:
MIT License - https://opensource.org/licenses/MIT
"""
import json
import base64
import sys
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
def base64url_decode(data):
"""Decode base64url to bytes"""
# Add padding if needed
padding = 4 - len(data) % 4
if padding != 4:
data += '=' * padding
# Replace URL-safe chars
data = data.replace('-', '+').replace('_', '/')
# Decode
return base64.b64decode(data)
def jwk_to_pem(jwk_key):
"""Convert JWK to PEM format"""
if jwk_key['kty'] != 'RSA':
raise ValueError("Only RSA keys are supported")
# Decode the modulus (n) and exponent (e) to int
n = int.from_bytes(base64url_decode(jwk_key['n']), 'big')
e = int.from_bytes(base64url_decode(jwk_key['e']), 'big')
# Create RSA public key
public_key = rsa.RSAPublicNumbers(e, n).public_key()
# Serialize to PEM
pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
return pem.decode()
def main():
if len(sys.argv) > 2:
print("Usage: jwk_to_pem.py [jwks.json]")
print("If no file is provided, reads from stdin")
sys.exit(1)
if len(sys.argv) == 2 and sys.argv[1] != '-':
# Read from file
with open(sys.argv[1], 'r') as f:
jwks = json.load(f)
else:
# Read from stdin
jwks = json.load(sys.stdin)
# Convert each key
for i, key in enumerate(jwks['keys']):
kid = key.get('kid', f'key-{i}')
print(f"# Key {i} (kid: {kid})")
print(jwk_to_pem(key))
if __name__ == "__main__":
main()
</pre>
Connect MCP Servers to Junie in PhpStorm - PhpStorm : The IDE that empowers PHP developers | The JetBrains Bloghttps://blog.jetbrains.com/?post_type=phpstorm&p=5964572025-09-05T14:47:16.000Z
<p>The <strong>Model Context Protocol (MCP)</strong> is an open standard introduced by <a href="https://docs.anthropic.com/en/docs/mcp" data-type="link" data-id="https://docs.anthropic.com/en/docs/mcp" target="_blank" rel="noopener">Anthropic</a>. Think of it as a USB-C port for AI: a consistent way to plug the AI models your AI agent uses into specific tools and data sources. </p>
<p>You can connect <a href="https://www.jetbrains.com/junie/" target="_blank" rel="noopener">Junie</a>, the AI coding agent by JetBrains, to a wide variety of <a href="https://github.com/modelcontextprotocol/servers?tab=readme-ov-file#-third-party-servers" target="_blank" rel="noopener">officially provided or community built MCP servers</a>, or build your own MCP server using one of the available SDKs – including the <a href="https://thephp.foundation/blog/2025/09/05/php-mcp-sdk" target="_blank" rel="noopener">brand-new MCP PHP SDK</a>.</p>
<h2 class="wp-block-heading">About MCP PHP SDK</h2>
<p>MCP SDKs are lightweight frameworks that handle the protocol details so that developers can focus on the application functionality that AI agents will use.</p>
<p><a href="https://github.com/modelcontextprotocol/php-sdk" target="_blank" rel="noopener">MCP PHP SDK</a> is the official MCP SDK built as a joint effort by <strong>the </strong><a href="https://thephp.foundation/" target="_blank" rel="noopener"><strong>PHP Foundation</strong></a><strong>, Anthropic’s MCP team, and </strong><a href="https://symfony.com/" target="_blank" rel="noopener"><strong>Symfony</strong></a>. The goal of the project is to provide a <strong>framework-agnostic</strong>, production-ready reference implementation the PHP ecosystem can rely on. For details about using MCP PHP SDK or contributing to it, see <a href="https://github.com/modelcontextprotocol/php-sdk" target="_blank" rel="noopener"><strong>modelcontextprotocol/php-sdk</strong></a>.</p>
<h2 class="wp-block-heading">Junie + MCP</h2>
<p>Whether you are building your own MCP server or using an existing one, here’s how to make it work with the <a href="https://www.jetbrains.com/junie/" target="_blank" rel="noopener">Junie AI agent</a> in PhpStorm:</p>
<ol>
<li><strong>Make sure that the MCP server can be accessed and started. </strong><br>The specific way of starting an MCP server depends on this server’s implementation – please refer to the MCP server documentation for instructions.<br></li>
<li><strong>Configure Junie in PhpStorm to connect. </strong><br>To add the MCP server to Junie, press <em>⇧Shift</em> twice to open the search window and search for “MCP Settings”. On the <em>MCP Settings</em> page, you can see the list of connected servers and add or edit JSON configurations for the MCP servers in the <code>~/.junie/mcp.json</code> file.<br><br><img decoding="async" fetchpriority="high" class="wp-image-597167" style="width: 1280px;" width="2560" height="1440" src="https://blog.jetbrains.com/wp-content/uploads/2025/09/mcp_settings_junie.png" alt=""><br>To configure an MCP server at the project level, manually add an <code>mcp.json</code> file with your desired server configurations to the <code>.junie/mcp</code> folder in the project root.<br><br>To view the list of actions that Junie can perform through a configured MCP server, locate the server in the <em>MCP Settings</em> list and expand the <em>Status</em> drop-down list.<br><br><img decoding="async" class="wp-image-597178" style="width: 1280px;" width="2560" height="1446" src="https://blog.jetbrains.com/wp-content/uploads/2025/09/view_available_mcp_ser_tools.png" alt=""><br>For example, with the <a href="https://boost.laravel.com/installed" target="_blank" rel="noopener"><strong>Laravel Boost MCP server</strong></a>, Junie gets useful tools for working with Laravel projects, such as listing Artisan commands, routes, or config files, reading logs, querying the database, or searching versioned documentation.<br><br>This is where MCP shines: It bridges the gap between LLMs and dynamic, framework- and project-specific context and data.<br></li>
<li><strong>Start using new commands and context inside your IDE. </strong><br>When running a prompt, Junie analyzes what commands registered with the configured MCP servers are relevant, and executes them through the respective MCP server.<br><br><img decoding="async" class="wp-image-597189" style="width: 1280px;" width="2564" height="1454" src="https://blog.jetbrains.com/wp-content/uploads/2025/09/laravel-boost-junie.png" alt=""></li>
</ol>
<p>The Junie coding agent keeps evolving. Join the conversation in our <a href="https://jb.gg/junie/web" target="_blank" rel="noopener">Junie Discord community</a> and help shape what’s next for Junie.</p>
<h2 class="wp-block-heading">New chapter for MCP servers in PHP</h2>
<p>While you could use any SDK implementation to create MCP servers for PHP projects, the official <a href="https://github.com/modelcontextprotocol/php-sdk" target="_blank" rel="noopener">MCP PHP SDK</a> boosts the contribution of the PHP community into the AI ecosystem, both within the PHP realm and beyond.</p>
<p>The PHP SDK maintainers encourage PHP developers to submit their MCP integrations for PHP frameworks and tools – Laravel, Symfony, WordPress, custom APIs, and more – to be listed in the <a href="https://github.com/modelcontextprotocol/php-sdk" target="_blank" rel="noopener">SDK GitHub repo</a>.</p>