PHP - BlogFlock All things PHP 2025-10-28T05:08:42.195Z BlogFlock Rob Allen, PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog, Exakat, Blog entries :: mwop.net The State of PHP 2025 - PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog https://blog.jetbrains.com/?post_type=phpstorm&p=646639 2025-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&#8217;s great to see more newcomers in PHP. It&#8217;s not surprising given the amount of positive buzz around PHP for the past couple of years, but it&#8217;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&#8217;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&#8217;s survey really reflects the quality of our recent releases.<br /> <br /> Since the survey, we&#8217;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&#8217;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&#8217;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 Allen https://akrabat.com/?p=7516 2025-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 Allen https://akrabat.com/?p=7512 2025-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 Blog https://blog.jetbrains.com/?post_type=phpstorm&p=645602 2025-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.&nbsp;</p> <p>Let’s work together to make PHP even better!</p> Opening Slack Jira Cloud links in the right browser - Rob Allen https://akrabat.com/?p=7499 2025-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.net https://mwop.net/blog/2025-09-23-zoom-disabling-mini-window.html 2025-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&#039;Phinney</a>. </div> Fixing PostgreSQL collation version mismatch - Rob Allen https://akrabat.com/?p=7493 2025-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 Allen https://akrabat.com/?p=7490 2025-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+&gt;</tt>. It doesn't matter where you are in your history, pressing <tt>meta+&gt;</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+&gt;</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 - Exakat https://www.exakat.io/?p=15983 2025-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 Allen https://akrabat.com/?p=7484 2025-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 Blog https://blog.jetbrains.com/?post_type=phpstorm&p=596457 2025-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 &#8220;MCP Settings&#8221;. 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.&nbsp;</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> Stop in-place editing of bash history items - Rob Allen https://akrabat.com/?p=7480 2025-09-02T10:00:00.000Z <p>Recently, since getting a new computer, I've noticed that I've been losing bash history items and it took a while to work out what was going on, though I'm still not completely sure as it never seemed to be so much of a problem. </p> <p>I regularly use the up and down keys with <a href="/context-specific-history-at-the-bash-prompt/">context specific history</a>. For example, I will type <tt>ma</tt> and then press up to search back through all the <tt>make</tt> commands I've used recently and then press enter to run it. </p> <p>Sometimes, I'll realise that I don't actually want this command and edit it and press enter. Sometimes I'll decide halfway through editing that really I should use a <tt>docker compose</tt> command instead and I'll just back out of my edit via some key stroke that works. I'm not sure what I do here though, probably up/down, or maybe ctrl+c. Whatever I do, sometimes, the history for that line is now my edited mess and not the original command. Then later, when I go to try and find it via the up arrow, it's missing.</p> <p>This happened infrequently enough that I thought I was misremembering what was in the history, or that maybe it was another tab I was thinking about.</p> <p>I never want the bash history to be editable; if I cancel out, then I want it back to what it was.</p> <h2>Fixing with revert-all-at-newline</h2> <p>This finally annoyed me enough that I sat down with the Internet to work out how to fix it with the <tt>revert-all-at-newline</tt> setting.</p> <p>The revert-all-at-newline option in bash controls whether readline reverts any changes made to a history line when you press Enter. Note that this is part of readline's behavior, so it affects command line editing in bash and other programs that use <a href="https://en.wikipedia.org/wiki/GNU_Readline">readline</a>.</p> <p>The simplest thing is to add this to <tt>.bashrc</tt>:</p> <pre lang="bash"> bind 'set revert-all-at-newline on' </pre> <p>Alternatively, you can create a <tt>.inputrc</tt> file with this in it:</p> <pre> $include /etc/inputrc set revert-all-at-newline on </pre> <p>To view the current value of <tt>revert-all-at-newline</tt>, use: </p> <pre> bind -V | grep revert-all-at-newline </pre> <p>It solved my problem, and I've not yet found a case when I want it set the other way.</p> Extending an OpenAPI Component Schema - Rob Allen https://akrabat.com/?p=7477 2025-08-26T10:00:00.000Z <p>One project that I'm working on uses <a href="https://www.rfc-editor.org/rfc/rfc9457.html">RFC 9457 Problem Details for HTTP APIs </a> for its error responses.</p> <p>In the OpenAPI spec, we can define this as a component and use in the relevant paths as appropriate:</p> <pre lang="yaml"> components: schemas: ProblemDetails: type: object properties: type: type: string format: uri-reference description: A URI reference that identifies the problem type default: about:blank example: https://example.com/probs/out-of-credit title: type: string description: A short, human-readable summary of the problem type example: You do not have enough credit. status: type: integer format: int32 description: The HTTP status code for this occurrence of the problem minimum: 100 maximum: 599 example: 403 detail: type: string description: A human-readable explanation specific to this occurrence of the problem example: Your current balance is 30, but that costs 50. instance: type: string format: uri-reference description: A URI reference that identifies the specific occurrence of the problem example: /account/12345/msgs/abc additionalProperties: true </pre> <p>When we return a validation error, we add an <tt>errors</tt> property. Rather than repeating the <tt>ProblemDetails</tt> properties into <tt>ValidationError</tt>, we can add the <tt>errors</tt> using <a href="https://json-schema.org/understanding-json-schema/reference/combining#allOf"><tt>allOf</tt></a>:</p> <pre lang="yaml"> ValidationError: allOf: - $ref: '#/components/schemas/ProblemDetails' - type: object properties: errors: type: object description: Field-specific validation error messages additionalProperties: type: string example: name: "name must be provided" dateOfBirth: "date must be in the past" </pre> <p>This is quite handy!</p> Saving the current URL to a Note - Rob Allen https://akrabat.com/?p=7467 2025-08-19T10:00:00.000Z <p>Inspired by John Gruber mentioning on the <a href="https://www.relay.fm/cortex/169">Cortex podcast</a> that he has a shortcut that saves links to a note in <a href="https://tot.rocks">Tot</a>, I thought I'd do something similar for saving to a note in Apple Notes.</p> <p>I want to store as a bullet item containing the name of the page, the link and the date. Something like this:</p> <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" /><br /> <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025saved-link-text-light.png" loading="lazy" alt="Saved link text light." class="border" width="408"/> </picture> <p>(Funny that the spellchecker doesn't know that Thu is the short form for Thursday)</p> <h2>The <em>Save Links to Notes</em> Shortcut</h2> <p>This is the shortcut that I created to do it: <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-shortcut-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-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/07/2025save-links-to-notes-shortcut-light.png" loading="lazy" alt="Save links to notes shortcut light." class="noborder" width="558"/> </picture> <p>You can download it here:<br /> <a href="https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b">https://www.icloud.com/shortcuts/6c12e888f9da431d9977985b229d636b</a></p> <h2>Breaking down the actions</h2> <p>To get the URL into the shortcut, we want:</p> <ul> <li><em>Show in Share Sheet</em> so that it's available on iOS/iPadOS</li> <li><em>Receive What's Onscreen</em> so that when a browser is focussed on Mac, it finds the URL</li> <li><em>Use as a Quick Action</em> so that we can assign a keyboard shortcut (<tt style="font-family: sans-serif">⌃⌥⌘U</tt> in case)</li> </ul> <p>We can then use <em>Get Contents of web page</em> along with <em>Get Details of Safari Web Page</em> to get the pages's title which Shortcuts calls <em>Name</em> for some reason.</p> <p>There's an action for <em>Current Date</em>, so we add that to get the variable.</p> <p>Creating formatted text in a note is a little involved. Firstly we use a <em>Text</em> action to set out the Markdown that we want. I used the date format <tt>EEE, dd MMM yyyy</tt> as it's short and clear to me.</p> <p>There's a <em>Make Rich Text from Markdown</em> action which processes the Markdown for us, but if you just append it to the note, it doesn't work. The workaround is to add it to a <em>List</em> action and then append the list to the note. </p> <h2>That's it</h2> <p>All we need to do now is show a notification including the <tt>Shortcut Input</tt> variable as that's the URL that we've just saved. <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" /><br /> <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025save-links-to-notes-notification-light.png" loading="lazy" alt="Save links to notes notification light." class="noborder" width="376"/> </picture> <p>With this shortcut, I can add a new entry to my note from both my Mac, iPad and iPhone with minimal effort. </p> <p>I like it.</p> Accessing Longplay info for SwiftBar - Rob Allen https://akrabat.com/?p=7447 2025-08-12T10:00:00.000Z <p>One app that I find incredibly useful is <a href="https://github.com/swiftbar/SwiftBar">SwiftBar</a> and one use I have is to display track info for the currently playing song in Apple Music.</p> <p>SwiftBar plugins work as shell scripts that execute on a timer and echo specially formatted text which SwiftBar then turns into an item on the menu bar with an attached menu</p> <p>I use a heavily modified <a href="https://github.com/matryer/xbar-plugins/blob/main/Music/nowplaying.5s.sh">Now Playing plugin</a> that was originally written by Adam Kenyon, so all the hard work was done by them.</p> <p>Recently, I've been using <a href="https://longplay.rocks">Longplay</a> to play albums and wanted the same functionality.</p> <p>Now Playing uses AppleScript to determine if a music player is playing and what the track info is:</p> <pre> app_playing=$(osascript -e "tell application \"$i\" to player state as string") </pre> <p>And</p> <pre> track=$(osascript -e "tell application \"$app\" to name of current track") artist=$(osascript -e "tell application \"$app\" to artist of current track") </pre> <p>When looking at adding Longplay, I was pleased to discover that it has AppleScript support, but perusing the Dictionary, I discovered that it doesn't support the features I need here.</p> <p>Upon emailing the developer, they very helpfully pointed out that Longplay also has Shortcuts support and that I could probably use that instead. They were right.</p> <p>I knocked up a couple of shortcuts:</p> <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" /><br /> <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-status-light.png" loading="lazy" alt="Longplay status Apple Shortcut" class="noborder" width="431"/> </picture> <p>and</p> <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" /><br /> <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025longplay-now-playing-light.png" loading="lazy" alt="Longplay now playing light." class="noborder" width="431"/> </picture> <p>With these set-up, I can now run them from the command line using <tt>shortcuts</tt>:</p> <pre> app_playing=$(shortcuts run "Longplay status"); </pre> <p>This will set <tt>$app_playing</tt> to either "Yes" or "No" as strings as it is defined as boolean in Shortcuts.</p> <pre> track=$(shortcuts run "Longplay now playing"); </pre> <p>This simply sets <tt>$track</tt> to the string of the currently playing track.</p> <h2>Updated Now Playing script</h2> <p>With the ability to get the info I needed from the command line, I <s>hacked</s> updated my copy of the Now Playing script and all is good.</p> <p>I've updated it a bit over the years, so I've uploaded my version to Gist: <a href="https://gist.github.com/akrabat/8bcfac9dfef5fd4e9b67ac5bb504ea7a">nowplaying.5s.sh</a>.</p> PhpStorm 2025.2 Is Now Available - PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog https://blog.jetbrains.com/?post_type=phpstorm&p=579514 2025-08-05T13:45:17.000Z <p>Along with <a href="https://blog.jetbrains.com/phpstorm/2025/07/laravel-idea-is-now-free/" data-type="link" data-id="https://blog.jetbrains.com/phpstorm/2025/07/laravel-idea-is-now-free/">making Laravel Idea part of PhpStorm</a>, this release brings improvements to the remote development experience, JetBrains AI tools, and more.</p> <p class="has-text-align-center"><a class="jb-download-button" href="https://www.jetbrains.com/phpstorm/download/" target="_blank" rel="noopener">Download PhpStorm 2025.2</a></p> <figure class="wp-block-image size-full"><img decoding="async" fetchpriority="high" width="2560" height="1440" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/PS-social-BlogFeatured-1280x720-2x-2.png" alt="" class="wp-image-579702"/></figure> <h2 class="wp-block-heading">Junie coding agent</h2> <h3 class="wp-block-heading">MCP support&nbsp;</h3> <p>Support for the <strong>Model Context Protocol (MCP)</strong> allows you to connect Junie to external sources like databases, file systems, and APIs. You can now add or edit the configuration for MCP servers at a global or project level in the IDE settings (<em>Tools</em> | <em>Junie</em> | <em>MCP Settings</em>).</p> <figure class="wp-block-image size-full"><img decoding="async" width="2560" height="1260" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/junie_mcp_updated.png" alt="" class="wp-image-584288"/></figure> <h3 class="wp-block-heading">WSL 2 support</h3> <p>Junie can now work with projects that are located under the WSL 2 file system&nbsp; (<code>\\wsl$\...</code> or <code>\\wsl.localhost\..</code> ) and opened in PhpStorm directly (via <em>File</em> | <em>Open</em>).</p> <figure class="wp-block-image size-full"><img decoding="async" width="2560" height="1440" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/junie_with_wsl2.png" alt="" class="wp-image-579526"/></figure> <h3 class="wp-block-heading">Remote development with Junie</h3> <p>With support for remote development environments, you can use the Junie plugin even if the IDE’s backend is running on a remote host.</p> <h3 class="wp-block-heading">30% speed increase</h3> <p>Junie can execute simple assignments, but it really shines when handling more complicated tasks, now with an <strong>up to 30% increase</strong> in prompt processing speed.&nbsp;</p> <p>Use <em>Code</em> mode to let Junie work on tasks autonomously, or switch to <em>Ask</em> mode<strong><em> </em></strong>to brainstorm about new features or solutions without making changes to your codebase.</p> <p class="has-text-align-center"><a class="jb-download-button" href="https://www.jetbrains.com/junie/" target="_blank" rel="noopener">Try Junie</a></p> <h3 class="wp-block-heading">Junie on GitHub EAP</h3> <p>We are excited to announce the opening of the EAP program for <strong><a href="https://www.jetbrains.com/junie-github/" target="_blank" rel="noopener">Junie on GitHub</a></strong>! </p> <p>Triggered from GitHub issues or issue comments, the Junie agent on GitHub has increased processing power and can handle multiple tasks simultaneously and without requiring the user to open the IDE.</p> <p>Sign up to join the Early Access Program and be the first to try Junie on GitHub.</p> <p class="has-text-align-center"><a class="jb-download-button" href="https://www.jetbrains.com/junie-github/#join-waitlist" target="_blank" rel="noopener">Join the waitlist</a></p> <h2 class="wp-block-heading">AI Assistant</h2> <p>JetBrains AI Assistant has also got a major upgrade. Whether you’re working online or offline, AI Assistant is now more capable, more flexible, and still free to use. </p> <p><em>All JetBrains AI features are available for free, with unlimited code completion, powerful local workflows, and limited cloud-based features.</em></p> <p><strong>Here’s what’s new:</strong></p> <ul> <li>Smarter completion across all supported languages, now with support for SQL, YAML, JSON, Markdown, and more.</li> </ul> <ul> <li>Expanded offline flexibility – connect any OpenAI-compatible model server like llama.cpp or LiteLLM.</li> <li>Project rules that give you the option to instruct AI to comply with your team’s coding conventions and business logic.</li> </ul> <p class="has-text-align-center"><a class="jb-download-button" href="https://www.jetbrains.com/ai-ides/#getstarted" target="_blank" rel="noopener">Get started</a></p> <h2 class="wp-block-heading">PHP</h2> <h3 class="wp-block-heading">Remote development in PhpStorm, now out of Beta</h3> <p>PhpStorm’s <a href="https://www.jetbrains.com/help/phpstorm/remote-development-overview.html" target="_blank" rel="noopener">remote development</a> functionality is out of Beta in version 2025.2, which means the experience of working with remotely hosted PhpStorm projects is steadily approaching that of working with source code locally. The removal of the Beta label comes along with quality improvements to:</p> <ul> <li>Remote editing.&nbsp;</li> <li>Debugger performance.</li> <li>Tool windows and dialogs, including the terminal, the VSC widget, <em>Search Everywhere</em>, and <em>Find in Files</em>.</li> <li>Support for Windows host machines via the JetBrains Toolbox App.</li> </ul> <figure class="wp-block-video"><video controls loop src="https://blog.jetbrains.com/wp-content/uploads/2025/07/remote_development.mp4"></video></figure> <h3 class="wp-block-heading">Reworked PHP <em>Include Path</em> dialog</h3> <p>In PhpStorm 2025.2, we’ve reworked the <em>Include Path</em> settings dialog to make it easier to <strong>exclude PHP library folders from indexing</strong> by improving navigation and search.</p> <p>Despite being excluded from error checks, PHP libraries are still <a href="https://www.jetbrains.com/help/phpstorm/indexing.html" target="_blank" rel="noopener">indexed</a>. On the <em>Include Path</em> settings page, you can exclude specific folders within PHP libraries from indexing.</p> <p>Now the <em>Include Path</em> list shows only parent directories by default, and to exclude a specific child directory from indexing, you need to select its parent in the list and click the <em>Exclude Under This Path</em> icon on the toolbar.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2560" height="1440" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/php_include_path.png" alt="" class="wp-image-579551"/></figure> <h3 class="wp-block-heading">PHPUnit 12 support</h3> <p>PhpStorm 2025.2 supports all the <a href="https://github.com/sebastianbergmann/phpunit/blob/12.0.0/ChangeLog-12.0.md#1200---2025-02-07" target="_blank" rel="noopener">changes and deprecations</a> introduced in PHPUnit 12. The IDE’s warnings and inspections will help you seamlessly upgrade projects to the latest version of the testing framework.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2560" height="530" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/PHPUnit_12_deprcated_methods_short.png" alt="" class="wp-image-579665"/></figure> <h3 class="wp-block-heading">Other improvements</h3> <ul> <li>The <a href="https://plugins.jetbrains.com/plugin/17518-php-architecture" target="_blank" rel="noopener">PHP Architecture</a> and <a href="https://plugins.jetbrains.com/plugin/27909-robo-support" data-type="link" data-id="https://plugins.jetbrains.com/plugin/27909-robo-support" target="_blank" rel="noopener">Robo Support</a> plugins are now unbundled, meaning they are no longer shipped with PhpStorm out of the box. If you need project architecture inspections or the functionality of the <a href="https://robo.li/" target="_blank" rel="noopener">Robo task runner</a>, you can <a href="https://www.jetbrains.com/help/phpstorm/managing-plugins.html#install_plugin_from_repo" target="_blank" rel="noopener">install</a> these plugins from JetBrains Marketplace.<br></li> </ul> <ul> <li>The <code>class-string&lt;T&gt;</code> type inference now properly displays the expected inferred type.</li> </ul> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="2680" height="1048" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/class_t_inference_short.png" alt="" class="wp-image-579676"/></figure> <ul> <li>PhpStorm 2025.2 further improves the <a href="https://blog.jetbrains.com/phpstorm/2025/01/support-for-env-files/">.env file support</a> with reference-aware renaming of nested variables.</li> </ul> <figure class="wp-block-video aligncenter"><video controls loop src="https://blog.jetbrains.com/wp-content/uploads/2025/07/env_rename.mp4"></video></figure> <ul> <li>Configuration options for PhpStorm’s built-in server, the always-running web server for static content like JavaScript, CSS, and HTML,<strong> </strong>have been moved to <em>Tools</em><strong> </strong>|<strong> </strong><em>Web</em><strong> </strong><em>Browsers and Preview</em> in the IDE settings.</li> </ul> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="3452" height="2032" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/built-in-server-settings.png" alt="" class="wp-image-579584"/></figure> <h2 class="wp-block-heading">JavaScript and TypeScript</h2> <h3 class="wp-block-heading">Experimental TypeScript-Go language server support</h3> <p>PhpStorm 2025.2 introduces experimental support for the new TypeScript-Go language server, bringing <strong>improved performance and modern architecture</strong> for TypeScript development.<br>You can enable it in your project by installing the @typescript/native-preview package as a dependency in place of typescript.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="800" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/TypeScript-GO-min.png" alt="" class="wp-image-580450"/></figure> <h3 class="wp-block-heading">Baseline support</h3> <p>PhpStorm 2025.2 now displays Web Platform Baseline <strong>information directly in quick documentation</strong>.When you hover over a web platform API, you&#8217;ll see details about when the feature became reliably available across major browsers, based on web.dev’s <a href="https://web.dev/blog/whats-new-in-web-io2025" target="_blank" rel="noopener">Baseline</a> data.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="800" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/Baseline-min.png" alt="" class="wp-image-580461"/></figure> <h3 class="wp-block-heading">Bun improvements</h3> <p>PhpStorm 2025.2 introduces smarter integration for Bun. When a <code>bun.lockb</code> or <code>bun.lock</code> file is present in your project, PhpStorm will <strong>automatically detect Bun and set it as the package manager</strong>. All relevant actions, such as running <code>bun install</code>, using context menu options for <code>package.json</code>, and resolving dependency suggestions, will default to Bun.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="800" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/Bun-package-manager-min.png" alt="" class="wp-image-580472"/></figure> <h2 class="wp-block-heading">User experience</h2> <h3 class="wp-block-heading"><em>Parameter Info</em> popup improvements</h3> <p>PhpStorm 2025.2 introduces several improvements that make the <em>Parameter Info</em> popup easier to read and navigate.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1600" height="800" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/Parameter-info-popup-min.png" alt="" class="wp-image-580483"/></figure> <h2 class="wp-block-heading">Databases</h2> <h3 class="wp-block-heading">Ability to attach database objects to the AI chat</h3> <p>The database context you provide to the AI Assistant’s chat can now be more specific. Previously, only the whole schema could be attached. Now, you can <a href="https://www.jetbrains.com/help/ai-assistant/ai-chat.html#attach_database_object" target="_blank" rel="noopener">attach the database object</a> you need to work with – for example, a table or a view.&nbsp;<br>To attach a database object, type <code>@</code> or <code>#</code> in the input field, select or type <code>dbObject:</code>, then select the object you want to attach from the list.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1500" height="600" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/non-dg_20252_aia_attach_db_object.png" alt="" class="wp-image-579648"/></figure> <h3 class="wp-block-heading">[SQLite] WSL database file path</h3> <p>PhpStorm now supports WSL file paths for SQLite database files.<br>This means that you can now access your SQLite database in WSL and work with it without the database file being locked for you. To do this, go to the <a href="https://www.jetbrains.com/help/datagrip/2025.2/data-sources-and-drivers-dialog.html" target="_blank" rel="noopener"><em>Data Sources and Drivers</em> dialog</a> and use the following file path format: <code>\\wsl$\&lt;os&gt;\home\&lt;username&gt;\&lt;database_file_name&gt;.sqlite</code>. For example, \<code>\wsl.localhost\Ubuntu-24.04\home\john.smith\identifier.sqlite</code>.</p> <figure class="wp-block-image size-full"><img decoding="async" loading="lazy" width="1500" height="600" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/non-dg_connectivity_wsl_file_path.png" alt="" class="wp-image-580429"/></figure> Responding to StreamDeck buttons with Keyboard Maestro - Rob Allen https://akrabat.com/?p=7440 2025-08-05T10:00:00.000Z <p>I run Apple Music on my Mac desktop and send the output to my HomePod minis. To control the volume, you need to manipulate the Apple Music volume slider rather than the global volume controls for the Mac.</p> <p>It's easier to press buttons than use a mouse, so I used <a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a> to respond to two buttons on my <a href="https://www.elgato.com/uk/en/p/stream-deck">Stream Deck</a> instead.</p> <p>This is possible because Keyboard Maestro has a <a href="https://marketplace.elgato.com/product/keyboard-maestro-35c7590b-b7fb-4be0-9e5d-9fd4b4c0f013">Stream Deck Plugin</a>, so you need to install that first.</p> <h2>Setting up the Stream Deck button</h2> <p>You can now assign Keyboard Maestro to a button in the Stream Deck software:</p> <p><img fetchpriority="high" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025streamdeck-km-button.png" alt="Keyboard Maestro automation configuration interface showing a button setup with a speaker/volume icon. The interface displays fields for Title (empty), Button ID (R3C1), Virtual Row (3), and Virtual Column (1). The left side shows a black square button with white speaker and minus icons." title="streamdeck-km-button.png" border="0" width="500" height="239" /></p> <p>This is the configuration for my volume down button, as you can tell by the icon I chose. The Button ID defaults to the row and column number of where you have placed it on the Stream Deck.</p> <h2>Responding to the button in Keyboard Maestro</h2> <p>On the Keyboard Maestro side, we need a macro that is trigged by the Stream Deck button. This is easy to do as it looks like a USB device key and you can press the button the Stream Deck and Keyboard Maestro will recognise it and fill in the correct details.</p> <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025streamdeck-km-control.png" alt="Screenshot of an Keyboard Maestro automation interface showing a "Music volume down" macro. The trigger is a Stream Deck R3C1 button press with modifiers. The action executes AppleScript code that decreases the Music app's volume by 1, with a minimum volume of 0." title="streamdeck-km-control.png" border="0" width="500" height="529" /></p> <p>Upon clicking the button, we simply run some AppleScript to control the Music app's volume.</p> <h2>That's it</h2> <p>That's all there is to responding to a button on the Stream Deck on a Mac. In this case, I'm using AppleScript, but Keyboard Maestro lets you do practically anything on the computer!</p> The Laravel Idea Plugin is Now Free for PhpStorm Users - PhpStorm : The IDE that empowers PHP developers | The JetBrains Blog https://blog.jetbrains.com/?post_type=phpstorm&p=585748 2025-07-30T16:53:03.000Z <p>Starting July 30, 2025, we’re making <strong>Laravel Idea free for PhpStorm users</strong>. If you already have the <a href="https://laravel-idea.com/" target="_blank" rel="noopener">Laravel Idea</a> plugin installed, you get full access to all plugin features at no extra cost.</p> <p>Laravel Idea is the smartest Laravel development environment based on PhpStorm. Developed by <a href="https://adelf.tech/about" target="_blank" rel="noopener">Adel Faizrakhmanov</a>, the plugin has <a href="https://plugins.jetbrains.com/plugin/13441-laravel-idea/analytics" target="_blank" rel="noopener">1.5M+ downloads</a> from JetBrains Marketplace and is highly appreciated by Laravel developers. It extends PhpStorm’s built-in Laravel support with a variety of developer productivity features, including:</p> <ul> <li>Powerful code generation.</li> <li>Advanced routing, validation, request fields, gates and policies, configs, translations, views, and a lot of other completion features.</li> <li>Excellent understanding of the Eloquent ORM.</li> <li>Full Blade component support.</li> <li>Support for Livewire, Inertia.js, Dusk, Laravel modules, and other third-party packages.</li> </ul> <blockquote class="wp-block-quote"> <div class="blockquote"> <blockquote><p>“When I started Laravel Idea, I couldn&#8217;t even dream that it would be included in PhpStorm! And now&#8230; It&#8217;s happening! I&#8217;m very happy that it has become more accessible for Laravel developers.<br /> <br /> My team and I will continue working on Laravel Idea the same way as before. Besides that, we will work closely with the PhpStorm team on many features, like AI, native support for Laravel magic, etc.”</p></blockquote> <div class="blockquote__author"> <img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/01/adel_photo_small.jpg" alt="Adel Faizrakhmanov"> <div class="blockquote__author-info"> <strong class="blockquote__author-title">Adel Faizrakhmanov</strong> <span class="blockquote__author-subtitle">Creator of Laravel Idea</span> </div> </div> </div> </blockquote> <p>To install the Laravel Idea plugin in PhpStorm, go to <em>Settings</em> | <em>Plugins</em> and search for the plugin in the <em>Marketplace</em> tab.</p> <figure class="wp-block-image size-full"><img decoding="async" fetchpriority="high" width="2240" height="1060" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/laracel_idea_plugin_in_marketplace_tab.png" alt="" class="wp-image-587317"/></figure> <p>After installation, you can configure the plugin settings via <em>Settings</em> | <em>Languages &amp; Frameworks</em> | <em>Laravel Idea</em>.</p> <h2 class="wp-block-heading">Why PhpStorm IDE for Laravel development?</h2> <p>PhpStorm is <strong>recommended by Laravel </strong>for working on Laravel projects. Besides the Laravel Idea plugin, PhpStorm provides Laravel developers with loads of developer productivity features out of the box, including:</p> <ul> <li><a href="https://www.jetbrains.com/junie/" data-type="link" data-id="https://www.jetbrains.com/junie/" target="_blank" rel="noreferrer noopener">Junie</a>, your AI coding agent by JetBrains for professional tasks.&nbsp;</li> <li>Built-in support for Laravel Pint, Pest, Larastan, and Artisan CLI commands.</li> <li>Syntax highlighting and code completion in Blade templates and <code>.env</code> files.&nbsp;</li> <li>IDE support for JavaScript/TypeScript frameworks, including Tailwind, Vue, and React, and databases.&nbsp;</li> <li>Best-in-class tools for code navigation and refactoring.</li> </ul> <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="PhpStorm Laravel Tips" width="500" height="281" src="https://www.youtube.com/embed/S-D__wiHZpQ?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>The Laravel Idea plugin will eventually be bundled with PhpStorm and available to the IDE users out of the box.</p> <div class="blockquote"> <blockquote><p>“I’m really excited that full Laravel support is now more accessible than ever for our users. We’ve worked hard to make PhpStorm the best IDE for Laravel development, and making Laravel Idea available for free is a big step forward.”</p></blockquote> <div class="blockquote__author"> <img decoding="async" class="blockquote__author-img" src="https://blog.jetbrains.com/wp-content/uploads/2025/07/T0288D531-UBDED5C6N-2d6a0e5fa238-512.png" alt=""> <div class="blockquote__author-info"> <strong class="blockquote__author-title">Artemy Pestretsov</strong> <span class="blockquote__author-subtitle">Product Leader at PhpStorm</span> </div> </div> </div> <p>Learn more about Laravel support in PhpStorm <a href="https://www.jetbrains.com/phpstorm/laravel/" target="_blank" rel="noreferrer noopener">on our website</a>.</p> <h2 class="wp-block-heading">FAQ</h2> <h3 class="wp-block-heading"><strong>I have an IntelliJ Ultimate subscription, do I also get the Laravel Idea plugin for free?</strong></h3> <p>Yes, the Laravel Idea plugin is now also available to IntelliJ Ultimate users for free.</p> <h3 class="wp-block-heading"><strong>Is there any compensation for already purchased Laravel Idea licenses?</strong></h3> <p>Individual PhpStorm users who purchased a monthly or yearly personal Laravel Idea license <em>on May 1, 2025, or later</em> are eligible for compensation in the form of a <em>50% discount</em> on the next renewal of their personal PhpStorm subscription. This compensatory discount will be applied in your <a href="https://account.jetbrains.com/" target="_blank" data-type="link" data-id="https://account.jetbrains.com/" rel="noreferrer noopener">JetBrains Account</a> automatically at the billing date of the next subscription renewal.</p> <h3 class="wp-block-heading"><strong>Why aren’t all active Laravel Idea subscriptions eligible for compensation?</strong></h3> <p><em>May 1, 2025</em>, marks the date we internally decided to make the Laravel Idea plugin part of PhpStorm and started working on it. We believe it&#8217;s fair to provide compensation to the users who purchased licenses after that date, as we had not announced this decision publicly at that time. Users who purchased the yearly license before did so when we were still operating under the third-party plugin business terms.&nbsp;</p> <h3 class="wp-block-heading"><strong>Can I get a refund for my Laravel Idea purchase?</strong></h3> <p>Yes, you can apply for a refund. To do so, please contact <a href="https://www.jetbrains.com/support/sales/?fromMenu=&amp;responseType=email-sales" target="_blank" rel="noreferrer noopener">our Sales team</a> and send us the order reference number, invoice number, or other purchase-related information. For more information about refunds, see the <a href="https://sales.jetbrains.com/hc/en-gb/articles/115000913704-How-can-I-get-a-refund" target="_blank" rel="noreferrer noopener">Licensing and Purchasing FAQ</a> page.</p> <h3 class="wp-block-heading"><strong>My licensing case is not addressed in this FAQ. What should I do?</strong></h3> <p>Please contact our Support team at <a href="mailto:phpstorm-support@jetbrains.com" target="_blank" rel="noreferrer noopener">phpstorm-support@jetbrains.com</a> and provide the details of your case.</p> Step-debugging Docker Compose NestJS services - Rob Allen https://akrabat.com/?p=7454 2025-07-29T10:00:00.000Z <p>I'm working on a <a href="https://nestjs.com">NestJS</a> project that uses <a href="https://docs.nestjs.com/cli/monorepo#monorepo-mode">monorepo mode</a>. It consists of a number of separate microservice applications that each have their own <a href="https://www.docker.com">Docker</a> container that are managed in development using <a href="https://docs.docker.com/compose/">Docker Compose</a>.</p> <p>I like step-debugging in my IDE and so needed to set it up for this application. This is what I did.</p> <h2>The application setup</h2> <p>Each service in our project has its own container in docker-compose.yaml and we use a reverse proxy to have a single endpoint that routes requests to the correct microservice. Each custom <tt>Dockerfile</tt> runs <tt>ppm run start:dev:{service name}></tt> which is defined in <tt>package.json</tt> like this:</p> <pre> "start:dev:service1": "nest start service1 --watch --tsc --watchOptions.poll=1000 --preserveWatchOutput", </pre> <p>Using <tt>--watchOptions.poll=1000</tt> is just more reliable when running in Docker with volumes mounted into the container. We also set the <tt>--preserveWatchOutput</tt> flag to ensure that the service doesn't take control of the terminal as this is unhelpful when you have multiple services in play.</p> <h2>Set the apps up for debugging</h2> <p>We need to make some modification for step debugging. Firstly, I created a set of <tt>start:debug:{service name}</tt> scripts in <tt>package.json</tt> that look like this:</p> <pre> "start:debug:service1": "nest start service1 --debug 0.0.0.0:9229 --watch --tsc --watchOptions.poll=1000 --preserveWatchOutput", </pre> <p>We enabled the <tt>--debug</tt> flag to enable node's <tt>--inspect</tt> flag so that port <tt>9229</tt> is available to the debugger. However, buy default this is bound to <tt>127.0.0.1</tt> which is not useful in a container, so we bind to <tt>0.0.0.0:9229</tt> so that it's available outside the container.</p> <p>Next, we need to our new <tt>start:debug:{service name}</tt> scripts and expose port 92229 to our local environment for each service. We do this in <tt>docker-compose.override.yaml</tt>:</p> <pre lang="yaml"> services: service1: ports: - "9230:9229" command: pnpm run start:debug:service1 service2: ports: - "9231:9229" command: pnpm run start:debug:service2 </pre> <p>I don't tend to like binding to the default port as that invariably confuses me when I run some test thing locally, so I've picked ports starting from <tt>9230</tt> onwards for my services.</p> <p>Running <tt>docker compose up</tt> will now start the containers.</p> <h2>Debugging in WebStorm</h2> <p>To set up <a href="https://www.jetbrains.com/webstorm/">WebStorm</a> for step debugging, create a <em>Run/Debug configuration</em> entry of type <tt>Attach to Node.js/Chrome</tt> for each container.</p> <p>The settings for service1 are:</p> <ul> <li>Name: Debug service1</li> <li>Host: localhost</li> <li>Port: 9230</li> </ul> <p>For the other services, change the name and port.</p> <picture><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-dark.png" media="(prefers-color-scheme: dark)" /><source srcset="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-light.png" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" /><br /> <img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025webstorm-nodejs-debug-config-light.png" loading="lazy" alt="Webstorm nodejs debug config light." class="noborder" width="600"/> </picture> <p>From the Debugging dropdown at in the title bar select the service and press the green "bug" button. You'll see a "Debugger attached." message in the Docker logs.</p> <p>Attach a breakpoint and access the endpoint using <tt>curl</tt> or another HTTP client and the IDE should stop execution at the breakpoint.</p> <h2>Debugging in VS Code</h2> <p>To set up <a href="https://code.visualstudio.com">VS Code</a> for step debugging, create a <tt>.vscode/launch.json</tt> file in your project. It should look like this:</p> <pre lang="json"> { "version": "0.2.0", "configurations": [ { "name": "Debug service1", "type": "node", "request": "attach", "port": 9230, "address": "localhost", "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/app", "restart": true }, { "name": "Debug service2", "type": "node", "request": "attach", "port": 9231, "address": "localhost", "localRoot": "${workspaceFolder}", "remoteRoot": "/usr/src/app", "restart": true } ] } </pre> <p>Set <tt>remoteRoot</tt> to the directory within the Docker container where the project is mounted.</p> <p>Start debugging by selecting the <em>Run and Debug</em> pane in the left hand toolbar and choose the service from the dropdown at the top. Then press the green <em>Start debugging</em> button (or press F5).You'll see a "Debugger attached." message in the Docker logs.</p> <p>Attach a breakpoint and access the endpoint using <tt>curl</tt> or another HTTP client and the IDE should stop execution at the breakpoint.</p> <h2>That's it</h2> <p>Having done this, I can now enjoy step debugging this new-to-me codebase and understand what it does!</p> QuickSS: Screenshot the active window on Mac - Rob Allen https://akrabat.com/?p=7418 2025-07-22T09:00:00.000Z <p>Back in 2016, I wrote about using QuickGrab to take a screenshot of the active window via a single key press with no mouse use required.</p> <p>It's now 2025 and I'm still using this and Apple has announced that Rosetta 2 will be phased out in a couple of years. As QuickGrab is one of the few Intel-only apps I still use, I thought I'd recompile for Apple Silicon and it was then that I ran into trouble:</p> <pre> $ gcc -framework cocoa -x objective-c -o quickgrab quickgrab.m quickgrab.m:92:26: error: 'CGWindowListCreateImage' is unavailable: obsoleted in macOS 15.0 - Please use ScreenCaptureKit instead. </pre> <p><em>Obsoleted in macOS 15.0</em> isn't something you want to see! The APIs that QuickGrab use are no longer part of the SDK and so it cannot be compiled. Hence I decided to replace it with a Swift version and also take the opportunity to add additional features that I wanted.</p> <p>While on holiday, I wrote <a href="https://github.com/akrabat/QuickSS">QuickSS</a>. I spent some time playing around with ScreenCaptureKit, but couldn't get it to replicate the window shadows that the standard screenshot tool on <tt>shift+cmd+4</tt> does. To solve this I decided to use <tt>screencapture</tt> to take the screenshot which is provided by macOS. </p> <h2>Screenshot to file</h2> <p>To save a screenshot of the active window directly to a file, just run <tt>quickss</tt>. You probably want to select cmd+tab to a different window, so prefix with <tt>sleep</tt>:</p> <pre> rob@Caledonia QuickGrab (master *) $ sleep 3; quickss Capturing window ID: 5940 [Safari: 'Rob Allen – Pragmatism in the real world'] Screenshot saved to: /Users/rob/Downloads/Screenshot 2025-07-22 at 11.00.00.png </pre> <p>As it's my app, I made some changes to match the way I work with it. Firstly it defaults to saving the file to the Downloads folder and names it the same as the default screenshot utility. You can use <tt>--file</tt> to override this should you need to and <tt>--quiet</tt> will output just the filename which is useful for onward scripting, or displaying in notifications.</p> <h2>Screenshot directly to clipboard</h2> <p>I also added the ability to put the screenshot directly onto the clipboard with <tt>--clipboard</tt></p> <pre> $ sleep 3; quickss --clipboard Capturing window ID: 5940 [Safari: 'Rob Allen – Pragmatism in the real world'] Screenshot copied to clipboard </pre> <p>I can now paste directly into Slack/Discord/Messages/Messenger/WhatsApp/Signal/etc. (Yes, there are far too many of these services nowadays!)</p> <h2>Keyboard shortcut</h2> <p>Obviously, the best way to run this is to use global keyboard shortcuts. My preference is to use <tt style="font-family: sans-serif">⌃⌥⌘4</tt> to put the screenshot onto the clipboard and <tt style="font-family: sans-serif">⇧⌃⌥⌘4</tt> to save it to file.</p> <h3>Using Shortcuts.app</h3> <p>You can use the Shortcuts app for this and create a shortcut for copying to clipboard like this:</p> <p><img fetchpriority="high" decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025quickss-take-screenshot-to-cliboard.png" alt="Quickss take screenshot to cliboard." title="quickss-take-screenshot-to-cliboard.png" border="0" width="700" height="511" /></p> <p>The equivalent for saving to file is the same, except that you don't need the <tt>--clipboard</tt> parameter.</p> <h3>Using Alfred</h3> <p>I use <a href="https://www.alfredapp.com/">Alfred</a> for these sort of things, so I wrote a Workflow to do this based on the previous one:</p> <p><img decoding="async" src="https://akrabat.com/wp-content/uploads/2025/07/2025quickss-alfred-workflow.png" alt="Quickss alfred workflow." title="quickss-alfred-workflow.png" border="0" width="700" height="363" /></p> <p>It's downloadable from the <a href="https://github.com/akrabat/QuickSS/releases/latest">QuickSS latest release</a> page.</p> <h2>That's it</h2> <p>Over the years, I've found that having a global hotkey to screenshot the current active window is really helpful. I like it even more now that it goes directly to the clipboard.</p>