# Using Anonymous Classes in PHP As of PHP 7, you can now create quick throwaway objects for use within your projects. This can be especially useful for your automated tests, for instance, by allowing you to create quick implementations of your interfaces. Let's review a Notifier interface that we may have. We can easily create an anonymous implementation by using the new keyword followed by the keyword class. Let's look at a basic example below. **Example** ```php ``` If your block name consists of more than one word, separate the words with a hyphen, like block-name. Block names should never describe the appearance. ### Nesting Blocks You can nest your blocks any number of levels deep. For example, let's create a block that holds all of our product blocks. ```html
``` ### Element Within each product we'll want to set a few elements. Elements, similar to blocks describe the purpose. The names should never be describing the appearance, but more the idea of what information will go within each section. For instance, we'll create the following 3 elements within our product block. 1. Product Name 2. Product Price 3. Link For More Details Each element when using the BEM Methodology should be named starting with the name of the Block followed by 2 underscores and then the name of the element itself. block-name\_\_element-name Here's our new example with the 3 specified elements. ```html
Test Name
$20.00
Details
``` ### Nesting Elements Just like blocks, you can also nest elements within each other. One thing to note, is that an element can only be a part of a block. Never another element. Meaning you cannot define a hierarchy when creating the class names for your elements. For instance, this is invalid block-name\_\_element-name\_\_element-name Your element names should only ever be in the format of block-name\_\_element-name ## Modifier Lastly we have our modifiers, which describe the appearance of either elements or blocks. A modifier could be viewed as changing the default behavior of a particular block or element. Let's say we want one of our products to be featured and stand out with a background color. We can simply make a modifier class of .product\_is-featured. Note that a modifier is named using the Block or Element name followed by a single underscore and the name of the modifier. block-name\_modifier-name Here's an example with a Product block that is featured. ```html ``` One important thing to note is that a modifier cannot be used outside the context of its owner (block or element). ### Double Dash Instead of the Single Underscore You may also come across a few other naming conventions for modifiers which are perfectly acceptable. With some projects you'll see the use of the double dash when separating a block or element from its modifier. For instance, using the example from the above, here's how you would see it using the double dash syntax. ```html ``` You can read more about the [BEM naming conventions](https://getbem.com/naming/){rel=""nofollow""}. ## File Structure When writing your CSS, you may be using a tool such as Stylus or SASS which in that case it will be easy for you to break out each of the blocks and elements into their own files. 1. A block should always be within its own directory. In the case of our Product block, we'd simply create a product directory. 2. The elements of a block should be inside of the block's directory as their own directories. For instance, the product name element should have a directory for \_\_name. 3. Within each elements directory, you'd have the CSS file for that element. For instance for our product name we'd have a file for product\_\_product-name.css *Your file extensions may vary depending on the tools you're using.* 4. Modifier directories would be named starting with the single underscore. For instance \_is-featured This is the recommended file structure, but you're free to organize your project how you like if this doesn't seem like a good fit for you. ## Where BEM Fits in 2026 BEM is still a solid choice, especially on larger teams and in server-rendered, multi-page apps where your CSS lives apart from your markup. Since I first wrote this, a couple of popular alternatives have grown up alongside it: CSS Modules scope class names to a component automatically, and utility-first frameworks like Tailwind compose styles right in your markup. They are less competitors than different trade-offs, and BEM's naming discipline still pairs nicely with whatever custom CSS you write, even inside a Tailwind project. The convention covered above has aged well, so it is worth knowing whichever approach you reach for. # Build a QR Code Lambda and Call It From Laravel You do not need to know Python, and you do not even need an AWS account, to follow this. We are going to build one small, fun thing: a function that turns text into a QR code, run it on your own machine inside Docker, and call it from a Laravel app. I will explain the Python as we go, because if you write PHP, the ideas all rhyme. When you are ready to put it on AWS for real, the [next part](https://franktheprogrammer.com/articles/deploy-a-lambda-container-image-with-ecr-and-the-console) handles that. ## What we are building A Lambda is just a single function that AWS runs for you on demand. Ours takes some text or a URL and hands back a QR code image. We will package it as a Docker container image (so the image library's native pieces just work), and run it locally with AWS's **Runtime Interface Emulator**, the same runtime AWS uses in the cloud. That last detail matters: "it works locally" really means "it will work on Lambda." All you need installed is **Docker** and a **Laravel app** (Laravel 13, which wants PHP 8.3+). No AWS account for any of this part. ## A little Python first Here is the entire function. Save it as `app.py`. Read it once, then I will translate it from PHP-brain to Python-brain. ```python import base64 import io import qrcode def handler(event, context): data = event.get("data", "https://franktheprogrammer.com") img = qrcode.make(data) buffer = io.BytesIO() img.save(buffer, format="PNG") return { "data": data, "qr_base64": base64.b64encode(buffer.getvalue()).decode(), } ``` Line by line, in terms you already know: - `def handler(event, context):` is the entry point AWS calls, like a controller method. The body is everything indented under it. There are no braces and no semicolons; indentation is the syntax. - `event` is the input as a dict, Python's associative array. `event.get("data", "...")` is exactly PHP's `$event['data'] ?? '...'`: read the key, fall back to a default. - `qrcode.make(data)` builds the QR code and returns an image object. - We save that image into an in-memory buffer (`io.BytesIO()` is like `php://memory`) instead of a file, then base64-encode the raw bytes so they can ride inside JSON. - We `return` a dict, and AWS serializes it to JSON for whoever called us. No `$` on variables anywhere. If any of that syntax feels alien, my [Python for PHP Developers](https://franktheprogrammer.com/articles/python-for-php-developers) guide maps the basics (dicts, f-strings, types) one to one. You do not need it to finish this, but it helps. ## The dependencies and the Dockerfile The function needs the `qrcode` library, and we want PNG output, so we install the `pil` extra (that pulls in Pillow, the imaging library). Put this in `requirements.txt`: ```text qrcode[pil] ``` Then a `Dockerfile` next to `app.py`. AWS publishes base images that already know how to be a Lambda, so it is short: ```dockerfile FROM public.ecr.aws/lambda/python:3.13 COPY requirements.txt ${LAMBDA_TASK_ROOT}/ RUN pip install --no-cache-dir -r requirements.txt COPY app.py ${LAMBDA_TASK_ROOT}/ CMD ["app.handler"] ``` `${LAMBDA_TASK_ROOT}` is where Lambda looks for your code, and `CMD ["app.handler"]` points at the `handler` function in `app.py`. If you want the full story on the base image and choosing an architecture, the reference [Package a Python Lambda as a Docker Image](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) goes deep; here we keep moving. ## Build the image One command, from the folder with your three files: ```bash docker build -t qr-generator . ``` That produces a container image named `qr-generator`. On an Apple Silicon Mac this builds for arm64 by default, which is fine. ## Run it locally with the Runtime Interface Emulator Start the container: ```bash docker run --rm -p 9000:8080 qr-generator ``` The AWS base image bundles the Runtime Interface Emulator, so the container is now listening exactly like the real Lambda service. In a second terminal, post an event to it. That URL is fixed, and the literal word `function` is part of it: ```bash curl -s "http://localhost:9000/2015-03-31/functions/function/invocations" \ -d '{"data": "https://franktheprogrammer.com"}' ``` You get back: ```json { "data": "https://franktheprogrammer.com", "qr_base64": "iVBORw0KGgo..." } ``` To actually see the QR code, decode that field into a file: ```bash curl -s "http://localhost:9000/2015-03-31/functions/function/invocations" \ -d '{"data": "https://franktheprogrammer.com"}' \ | python3 -c "import sys, json, base64; open('qr.png', 'wb').write(base64.b64decode(json.load(sys.stdin)['qr_base64']))" ``` Open `qr.png` and scan it with your phone. That image came out of a function running in Docker on your laptop, with no cloud involved. ## Call it from Laravel Because the container is still listening, Laravel reaches it with a plain HTTP request, the same call our `curl` made. A controller: ```php use Illuminate\Support\Facades\Http; class QrController extends Controller { public function show() { $response = Http::post( 'http://localhost:9000/2015-03-31/functions/function/invocations', ['data' => 'https://franktheprogrammer.com'], ); return view('qr', ['qr' => $response->json('qr_base64')]); } } ``` `Http::post($url, $payload)` sends the JSON event to the running container, and `$response->json('qr_base64')` plucks that one field out of the reply. Hand it to a Blade view, where a data URI renders the PNG inline with no file on disk: ```html QR code ``` Load the page and the QR code appears, generated by your Python Lambda and rendered by your PHP app. The only thing to remember is that the container has to be running (`docker run`) for Laravel to reach it. This is your local development loop; in production you would call the deployed function instead, which is the next part. ## Make it yours Now that the loop is tight, edit `app.py`, rebuild, and re-run: ```bash docker build -t qr-generator . && docker run --rm -p 9000:8080 qr-generator ``` For example, swap the one-line `qrcode.make()` for the fuller API to size it up and brand the color: ```python qr = qrcode.QRCode(box_size=10, border=2) qr.add_data(data) img = qr.make_image(fill_color="#2f6bff", back_color="white") ``` Edit, rebuild, hit the endpoint, refresh Laravel. That is the entire inner loop, and none of it has touched AWS yet. You have built and run a real Lambda entirely on your machine and called it from Laravel. When you want to put it online so the deployed app (or anyone) can reach it, the next part creates the ECR repository, pushes this exact image, and sets up the function in the AWS Console. ::note{label="Build your first Lambda"} Part one of a two-part beginner walkthrough. Next: [Deploy a Lambda Container Image With ECR and the Console](https://franktheprogrammer.com/articles/deploy-a-lambda-container-image-with-ecr-and-the-console) :: # Claude Code Hooks, by Example Some instructions are too important to leave to a model's good intentions. "Install the dependencies before you run anything." "Never commit to main." "Run the formatter after every edit." You can put these in CLAUDE.md and Claude Code will usually follow them, but usually is not always, and for the things that genuinely have to happen, usually is not good enough. That is what hooks are for. A hook is a shell command the Claude Code harness runs itself when an event fires, deterministically, whether the model thinks about it or not. Here is a short, practical tour around two I actually run: one that gets a fresh cloud session ready to work, and one that keeps noisy output out of the context window. ## Advisory versus guaranteed Think of it as a spectrum from advisory to guaranteed. CLAUDE.md is advisory. The model reads it and almost always respects it, which is perfect for preferences and conventions: indentation, naming, architecture notes. But it runs through the model's judgment, so it can get compacted away on a long session or simply not acted on. Hooks are guaranteed. When the event fires, the command runs, full stop, no "the model forgot." That makes them the right tool whenever correctness depends on something happening every time: setup before work starts, a check before a commit, a transformation on every file Claude touches. If skipping it once would be a bug, it belongs in a hook. The rule of thumb: if it is a preference, write it down for the model; if it is a guarantee, wire it into a hook. ## How a hook is wired Hooks live in `.claude/settings.json` under a `hooks` key, grouped by the event that triggers them. The ones you will reach for most: `SessionStart`, `UserPromptSubmit`, `PreToolUse` and `PostToolUse` (around any tool call), and `Stop`. Each entry can carry a `matcher` to narrow which tools it applies to, plus the hooks to run: ```json { "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh" } ] } ] } } ``` Two mechanics make hooks more than shell aliases. Every hook receives a JSON payload on stdin describing the event, the session id, working directory, and for tool events the tool's input, so your script can decide what to do. And a hook can talk back by printing JSON to stdout to allow, block, or rewrite an action. The second example leans on exactly that. Two variables matter for portable hooks: `$CLAUDE_PROJECT_DIR` is the repo root, and `$CLAUDE_CODE_REMOTE` is set when the session runs in Claude Code on the web rather than on your machine. ## Example 1: install dependencies on session start Claude Code on the web spins up a fresh container per session. On any project with a build step it starts empty, no `node_modules`, so the first command Claude runs fails. This bit me, and a `SessionStart` hook is the fix. Put the script in `.claude/hooks/session-start.sh`: ```bash #!/bin/bash set -euo pipefail # Only run in Claude Code on the web; no-op on a local machine that is # already set up. if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi cd "$CLAUDE_PROJECT_DIR" # Prefer the pinned Node version, but do not fail the session if nvm is fussy. export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" if [ -s "$NVM_DIR/nvm.sh" ]; then . "$NVM_DIR/nvm.sh" nvm install 24 >/dev/null 2>&1 || true nvm use 24 >/dev/null 2>&1 || true fi # Idempotent, and it benefits from container caching. npm install ``` Then register it: ```json { "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" } ] } ] } } ``` A few deliberate choices. The `CLAUDE_CODE_REMOTE` gate lets local sessions, already set up, skip the work. The `npm install` is idempotent, so resuming costs nothing and benefits from container caching. The Node setup is best-effort: if nvm misbehaves it does not abort the session, since the build tolerates a slightly older Node. It runs synchronously, so the session waits until dependencies are ready, which is what you want when the next action might be a build. To start instantly and install in the background instead, a hook can opt into async mode by printing `{"async": true, "asyncTimeout": 300000}` first. Once this is committed to your default branch, every future web session arrives ready to work instead of stumbling on the first command. ## Example 2: filter noisy output before Claude reads it The most common way to burn through a context window is to let a giant blob of output land in it. Run the test suite and Claude reads thousands of lines to find the three that failed, and they sit in the window for the rest of the session. A `PreToolUse` hook can trim the firehose before Claude ever sees it. The trick: a `PreToolUse` hook can rewrite the tool's input. This script checks whether the Bash command is a test runner and, if so, pipes it through a filter that keeps only failures: ```bash #!/bin/bash input=$(cat) cmd=$(echo "$input" | jq -r '.tool_input.command') # If this is a test run, rewrite it to surface only failures. if [[ "$cmd" =~ ^(npm\ test|pytest|go\ test) ]]; then filtered="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:)' | head -100" echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered\"}}}" else echo "{}" fi ``` Wire it to Bash calls with a matcher: ```json { "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/filter-test-output.sh" } ] } ] } } ``` When the command is not a test run, the script prints `{}` and steps aside. When it is, `updatedInput` hands Claude the modified command, so a run that might have dumped tens of thousands of tokens of passing output returns a few hundred lines of failures. It is the same context discipline from [getting more out of Claude Code](https://franktheprogrammer.com/articles/getting-more-out-of-claude-code), except enforced rather than hoped for. ## Gotchas to watch for A few things that will trip you up the first time: - **Make the script executable.** `chmod +x .claude/hooks/your-hook.sh`, or the hook silently does nothing. - **Use `$CLAUDE_PROJECT_DIR` for paths.** A hook does not necessarily run from your repo root, so reference scripts and files through the variable rather than a relative path. - **Hooks run on the host, with your privileges.** They are real shell commands on your machine, so keep them fast, keep them safe, and do not put anything in a hook you would not run yourself. - **Gate web-only work.** Setup that only makes sense in a fresh cloud container should check `$CLAUDE_CODE_REMOTE` so it does not run locally. - **Async trades safety for speed.** Synchronous hooks guarantee the work finishes before the session proceeds; async starts the session faster but introduces a race if Claude depends on something the hook is still doing. Hooks can also be bundled into a [plugin](https://franktheprogrammer.com/articles/how-to-build-a-claude-code-plugin) to share them across a team without everyone hand-editing settings. But you do not need that to start. Pick the one thing in your workflow that must happen every time, and wire it into a hook. That is the idea in one line: stop hoping, start guaranteeing. ::note{label="Going further"} There are more events than the handful here, and hooks can do more than run shell commands. The full list and the JSON contract live in the official docs at [code.claude.com/docs](https://code.claude.com/docs/en/hooks-guide){rel=""nofollow""}. :: # Configuring Gulp On A New Project In this example, we'll go through configuring gulp for a new project. We'll be using yarn as the dependency manager. If you are not familiar with yarn, please refer to my tutorial for setting it up. [Click Here to Configure The Yarn Dependency Manager](https://franktheprogrammer.com/articles/yarn-fast-and-secure-dependency-management) ::note{label="From 2026"} Gulp-based pipelines like the one in this article have largely been superseded by [Vite](https://vite.dev/){rel=""nofollow""} and esbuild paired with plain `npm` scripts, which handle bundling, CSS pre-processing, and live reload out of the box with far less config. Gulp still works and remains useful for bespoke file-shuffling tasks, but it is much less common in new projects today. The concepts below (sources, pipes, destinations) are still a great way to understand how build tools think. :: ## Installing Gulp to a New Project Gulp may seem like a scary thing to wrap your head around at first, but it's actually quite easy to start using once you understand the basics. To start, let's go ahead and within your project or a blank directory run the following commands. ### Initialize a new project within a directory ```bash yarn init ``` ### Install Gulp ```bash yarn add gulp ``` Create a gulpfile.js in the root of your project with the following contents. ```javascript var gulp = require('gulp'); gulp.task('default', function() { // place code for your default task here }); ``` The gulpfile.js file is where all of your configurations will go, letting gulp know exactly how to work within your project. This is a basic example which does not actually accomplish anything, but instead is a great boilerplate for you to build off of. Now run gulp. ```bash gulp ``` By default when running this command it will run the task named 'default'. In order to run specific tasks you can verbosely write ```bash gulp ``` ### Example So now let's setup an example project using Twitter's Bootstrap framework. We'll want to start by creating a new directory where we will store our project. I normally do this by opening up a terminal and creating the directory using the following command. ```bash mkdir ``` You can also setup the directory however you feel comfortable. Once the directory is created we'll then need to change into it using cd \ or if you're not yet in a terminal open one up and navigate to it so that we can run the next set of commands. Once you're within the newly created directory for our project, we'll want to initialize with yarn in order to take advantage of it for handling our dependencies. ```bash yarn init ``` Fill in the various prompts in order to proceed to the next step. After we have yarn initialized, we'll then want to add 3 packages to our project: gulp, gulp-sass, and bootstrap-sass. To find packages that you might want to use, you can use the search on [npmjs.com](https://www.npmjs.com/){rel=""nofollow""}. First, we'll install bootstrap-sass ```bash yarn add bootstrap-sass ``` Secondly, let's get gulp installed. ```bash yarn add gulp ``` Lastly, let's install gulp-sass ```bash yarn add gulp-sass ``` This will allow us to convert the bootstrap sass files to actual css easily. You'll notice that now if you open your package.json file, you'll see those 3 packages now added as dependencies for your project. Now we're ready to start configuring our gulpfile.js to utilize our new dependencies. The first thing we'll want to do is get the files that need to be copied from our newly created node\_modules folder. The directory node\_modules is where all of your dependencies are stored when you install them via the yarn or npm command. You should never edit the files directly within that directory as they can be overridden with future updates from their publishers. First I'd recommend adding the path to your node\_modules/.bin/ directory to your environment variable on your system this way you can just run gulp by itself instead of ./node\_modules/.bin/gulp Once that is setup, then we can setup the gulpfile.js file to first copy our bootstrap files to our new locations. First, within the gulpfile.js file, we'll want to import the plugins that we will be using at the top as such. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); ``` Now that we have our required plugins included, we'll then want to define our task. For this example, we'll just stick to using the default task as so. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); gulp.task('default', function() { }); ``` Next, up we want to copy our bootstrap-sass files to our new location. We'll set them up in the path assets/sass/bootstrap/ for now. Feel free to organize the directories however you like for your own projects. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); gulp.task('default', function() { // Copy SASS files to new location gulp.src([ './node_modules/bootstrap-sass/assets/stylesheets/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*/*', ]).pipe(gulp.dest('./assets/sass/bootstrap/')); }); ``` You'll see that we defined the source of the files we want to copy by using .src, and passing an array of the directories we want to copy. The asterisk is a wildcard stating you want everything within the designated directory. After we have the source, we then need to pipe it to the next command which defines the new destination that all of the previous files should be copied to. Next let's create a file within our assets/sass/ directory for custom.scss. We'll use this file to place any of our custom sass code, while also importing the bootstrap library at the top. ```sass @import 'bootstrap/bootstrap'; body { background-color: green; } ``` Lastly, we'll want to convert our new SASS file to CSS, in order to use it within our project. We'll accomplish that by using the following code within our default task block. ```javascript // Convert SASS to CSS gulp.src('./assets/sass/custom.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./css')); ``` You'll note that we are exporting the final usable CSS to a directory named css. You can update the path to your liking. Here's our final version of our gulpfile.js file for your reference. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); gulp.task('default', function() { // Copy SASS files to new location gulp.src([ './node_modules/bootstrap-sass/assets/stylesheets/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*/*', ]).pipe(gulp.dest('./assets/sass/bootstrap/')); // Convert SASS to CSS gulp.src('./assets/sass/custom.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./css')); }); ``` The only thing left now is to run the gulp command. Depending on whether or not you setup your environment variable you'd run one of the following commands. ```bash gulp # or, if you skipped the PATH step: ./node_modules/.bin/gulp ``` I hope this example helps jump start you into using gulp. It is a super powerful tool and can improve the way you develop applications dramatically. ## Bonus: Minifying Your CSS As a bonus, let's go ahead and add another plugin which will create a minified version of your CSS files. This can also be used to minify JS files if necessary. First, we'll want to add another plugin gulp-clean-css ```bash yarn add gulp-clean-css ``` Next, we'll need to include the plugin at the top of our gulpfile.js file. ```javascript var cleanCSS = require('gulp-clean-css'); ``` Now within our default task, we can add the code needed to handle the minifying as well as assuring the CSS we write is supported in ie8. ```javascript gulp.src('css/*.css') .pipe(cleanCSS({ compatibility: 'ie8' })) .pipe(gulp.dest('dist')); ``` You'll note that I'm now outputting the file to a dist folder to separate it from our non minified version. After you've made the required changes to the gulpfile.js file, then you can run the gulp command to process your changes. You'll want to run gulp each time you make a change to your CSS files. You could also make a watcher to look for changes in your files, this way it happens automatically while developing. I cover setting up a watcher in a separate tutorial: [Gulp Watch: Automate Your Gulp Tasks](https://franktheprogrammer.com/articles/gulp-watch-automate-your-gulp-tasks). Here's the full file of everything we've done so far. Now go have some fun with gulp. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); var cleanCSS = require('gulp-clean-css'); gulp.task('default', function () { // Copy SASS files to new location gulp.src([ './node_modules/bootstrap-sass/assets/stylesheets/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*/*', ]).pipe(gulp.dest('./assets/sass/bootstrap/')); // Convert SASS to CSS gulp.src('./assets/sass/custom.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./css/')); // Create Minified CSS Versions With Support for ie8 gulp.src('css/*.css') .pipe(cleanCSS({compatibility: 'ie8'})) .pipe(gulp.dest('dist')); }); ``` # Configuring Gulp With Less CSS Pre-Processor Less is a CSS pre-processor allowing you to create variables, mixins, and functions in an effort to make your CSS more maintainable. In this article, we'll discuss the process of getting less configured with gulp so that you can start using this CSS pre-processor today within your own projects. In this article, I touch on the basics of configuring less with gulp. For more detailed information on the configuration options available, read the [Less documentation](https://lesscss.org/){rel=""nofollow""}. ::note{label="From 2026"} A standalone Gulp plus gulp-less setup is uncommon today; modern tooling like [Vite](https://vite.dev/){rel=""nofollow""} handles Less out of the box. The Less language itself is still perfectly usable. :: ## Installation First, let's install gulp. ```bash yarn add gulp ``` Next, we'll install gulp-less ```bash yarn add gulp-less ``` ## Configuration Now that we have our packages installed, we'll want to setup our gulpfile.js to use these new plugins. ```javascript var gulp = require('gulp'); var less = require('gulp-less'); gulp.task('default', function() { }); ``` Next, let's create our less file. We'll store it in the path assets/less/custom.less. ```less @fontColor: #000101; @priceColor: green; .product { background-color: #aaaaaa; color: @fontColor; &--featured { background-color: #009998; } &__name { color: @fontColor; } &__price { color: @priceColor; } } ``` Now that we have our example less file, let's go ahead and configure our gulp file to process it. Within our gulpfile.js default block we'll want to add the following. ```javascript return gulp.src('./assets/less/*.less') .pipe(less()) .pipe(gulp.dest('./build/css/')); ``` In the above example you're passing the source first, and then passing those files to the less() function, and lastly letting gulp know where to put the output. In our example, we're storing the output to a ./build/css/ directory. Here's the full contents to our gulpfile.js file for reference. ```javascript var gulp = require('gulp'); var less = require('gulp-less'); gulp.task('default', function() { return gulp.src('./assets/less/*.less') .pipe(less()) .pipe(gulp.dest('./build/css/')); }); ``` ## Testing Now that we have everything setup, we'll want to run our gulp command. ```bash gulp ``` This should create a new file for custom.css within our ./build/css/ directory. I have included the contents of our custom.css, which was created during the gulp process for you to see how gulp transforms less into CSS. ```css .product { background-color: #aaaaaa; color: #000101; } .product--featured { background-color: #009998; } .product__name { color: #000101; } .product__price { color: green; } ``` This tutorial was not setup to teach you less, but instead to teach you how to start using LESS with gulp. If you do want to learn more about LESS, then I'd suggest reading through the documentation for a detailed explanation [here](http://lesscss.org/){rel=""nofollow""}. # Configuring Stylus CSS Pre-Processor with Gulp and Sourcemaps In this article we'll go over how to configure your project to process Stylus files using Gulp. We'll also create a source map file which your browser will use to help point you in the right direction of your files when developing. ::note{label="From 2026"} A dedicated Gulp plus gulp-stylus pipeline is rarely set up by hand today, since tools like [Vite](https://vite.dev/){rel=""nofollow""} handle Stylus and source maps automatically. The Stylus language still works fine. :: ## Installation If you're in a new project, with no package.json file present, you'll want to start off by typing yarn init to get this file setup. Now let's get gulp installed. ```bash yarn add gulp ``` Secondly, we'll want to install gulp-stylus. ```bash yarn add gulp-stylus ``` Lastly, let's install gulp-sourcemaps, this way when we're developing and using our development tools within the browser we'll be able to easily pinpoint which file our code resides in. ```bash yarn add gulp-sourcemaps ``` ## Configuration Now that we have our packages installed, let's start by setting up our base gulpfile.js file. ```javascript var gulp = require('gulp'); var stylus = require('gulp-stylus'); var sourcemaps = require('gulp-sourcemaps'); gulp.task('default', function() { return gulp.src('./assets/styl/main.styl') .pipe(sourcemaps.init()) .pipe(stylus()) .pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../../assets/styl/' })) .pipe(gulp.dest('./build/css/')); }); ``` Here's a breakdown of what is going on above. 1. We pass the source for where our stylus data can be found. ./assets/styl/main.styl 2. We then initialize our source maps plugin. 3. Next we'll write the source maps file as an external file to the current directory, while also changing the root of the document. 4. Finally, we'll output the rendered stylus file to our destination in ./build/css/. Now every time you run the gulp command, the above will generate a new CSS file, along with the new corresponding source map file. # Creating Configuration Files In Stylus ::note{label="Stylus series"} Part of my [Learning Stylus](https://franktheprogrammer.com/articles/learning-stylus-a-css-pre-processor) series. Stylus still works in 2026, but its ecosystem has largely moved on to Sass, PostCSS, and native CSS; the series intro has the full context. :: It's super simple to create a configuration file for instance that would manage your media query break points. You could also use a configuration file for managing colors, font sizes, and other variables such as gutter spacing and more. For this example, we'll look at creating 2 json configuration files to store our variables. JSON File: media-query-break-points.json ```json { "mobile": "screen and (max-width:400px)", "tablet": { "landscape": "screen and (min-width:600px) and (orientation:landscape)", "portrait": "screen and (min-width:600px) and (orientation:portrait)" } } ``` JSON File: colors.json ```json { "headerColor": "blue", "fontColor": "black" } ``` Now we can include our 2 json files, and reference the variables we created within. **Stylus Code** ```stylus json('media-query-break-points.json') json('colors.json') body color: fontColor .page-header color: headerColor padding: 50px 0 @media tablet-landscape padding: 25px 0 @media mobile padding: 10px 0 ``` # Deploy a Lambda Container Image With ECR and the Console In the [previous part](https://franktheprogrammer.com/articles/build-a-qr-code-lambda-and-call-it-from-laravel) we built a QR code Lambda, ran it locally in Docker, and called it from Laravel, all without an AWS account. Now we put it online. This part is mostly clicking through the AWS Console, with a couple of Docker commands you have already run. You will need an AWS account, the AWS CLI configured (`aws configure`), and Docker running with the `qr-generator` image you built last time. The path is short: store the image in Amazon ECR (Elastic Container Registry, AWS's private Docker registry), then create a Lambda from it. ## Create an ECR repository A repository is just a named home for one image and its tags. 1. Open the **Amazon ECR console** and pick your **Region** (top right). Remember which one; it has to match your function later. 2. Choose **Private repositories**, then **Create repository**. 3. For **Repository name**, enter `qr-generator` (lowercase; it may include a namespace like `team/qr-generator`). 4. For **Image tag immutability**, leave it **Mutable** so you can re-push `:latest` while iterating. 5. Leave **Encryption** on the default **AES-256**. 6. Choose **Create**. That is the entire repository. It is empty until we push an image into it. ## Push your image Select the new repository and choose **View push commands**. The console prints four commands with your account ID and Region already filled in. They log Docker in to your registry, then build, tag, and push: ```bash aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com docker build -t qr-generator . docker tag qr-generator:latest 111122223333.dkr.ecr.us-east-1.amazonaws.com/qr-generator:latest docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/qr-generator:latest ``` Swap `111122223333` for your account ID (the console does this for you). You already built the image in the previous part, so the `docker build` step just confirms it is current. When `docker push` finishes, refresh the repository in the console and you will see your image with the `latest` tag. ## Create the Lambda from the image Now point a function at that image. 1. Open the **Lambda console** and choose **Create function**. 2. Pick **Container image** (not "Author from scratch"). 3. For **Function name**, enter `qr-generator`. 4. For **Container image URI**, choose **Browse images**, select the `qr-generator` repository, pick the `latest` tag, and choose **Select image**. 5. For **Architecture**, choose the one your image was built for. On an Apple Silicon Mac that is **arm64**; on an Intel machine it is **x86\_64**. It has to match the image, because a Lambda image is built for exactly one architecture. 6. Choose **Create function**. A few things to know. The ECR repository must be in the **same Region** as the function. Right after you create it, the function sits in a **Pending** state for a few seconds while Lambda optimizes the image, then flips to **Active**; you cannot invoke it until it is Active. And Lambda needs permission to pull the image: when the repository is in the same account, it adds that permission for you automatically as long as your user is allowed to set repository policies. ## Configure and test Open the function. Under the **Configuration** tab you can bump **Memory** (try 512 MB) and **Timeout** (30 seconds is plenty), though the defaults work for our tiny function. Then the satisfying part. Go to the **Test** tab, create a new test event, and give it the same shape we used locally: ```json { "data": "https://franktheprogrammer.com" } ``` Choose **Test**. You will see the function's JSON response, including the `qr_base64` field, plus the execution logs and the duration. That is your container, running on AWS, producing the same result it did on your laptop. ## Going further: a Function URL your Laravel app can call The console Test is great, but your Laravel app wants a real endpoint. The simplest one is a **Function URL**, a dedicated HTTPS address for this function with no API Gateway needed. 1. On the function, open the **Configuration** tab, choose **Function URL**, then **Create function URL**. 2. For **Auth type**, choose **NONE** for a quick public demo (anyone with the URL can call it, so treat it as semi-secret), or **AWS\_IAM** to require signed requests. 3. Optionally set **CORS**, then **Save**. You get a URL like `https://abc123.lambda-url.us-east-1.amazonaws.com`. A Function URL delivers the request as an HTTP event, which wraps your posted JSON in a `body` string rather than passing it as the top-level event. Two extra lines make the handler accept both shapes, so it still works for the console Test and an SDK call: ```python import base64 import io import json import qrcode def handler(event, context): # A Function URL wraps the POST body as a JSON string; a direct invoke passes it as-is. if event.get("body"): event = json.loads(event["body"]) data = event.get("data", "https://franktheprogrammer.com") img = qrcode.make(data) buffer = io.BytesIO() img.save(buffer, format="PNG") return { "data": data, "qr_base64": base64.b64encode(buffer.getvalue()).decode(), } ``` Rebuild and push the image, redeploy (see below), and Laravel just swaps the local emulator URL for the Function URL: ```php use Illuminate\Support\Facades\Http; $response = Http::post('https://abc123.lambda-url.us-east-1.amazonaws.com', [ 'data' => 'https://franktheprogrammer.com', ]); $qr = $response->json('qr_base64'); ``` Returning a plain array from the handler lets the Function URL auto-wrap it as a `200` JSON response, so `$response->json('qr_base64')` reads exactly like it did locally. If you want the endpoint to return the raw PNG instead (so hitting the URL in a browser shows the image), the reference [Give Your Lambda an HTTP Front Door](https://franktheprogrammer.com/articles/give-your-lambda-an-http-front-door) shows the response shape for that. ## Updating later When you change the code, rebuild and push the image again (the same push commands), then tell the function to pick it up: on the function, choose **Deploy new image** (under the **Image** section), **Browse images**, select the new image, and **Save**. A tag like `latest` resolves to a specific image version at deploy time, so pushing alone does not update a running function; the redeploy is what repoints it. That is the journey end to end: a function you wrote and ran on your laptop is now live on AWS, testable in the console, and callable from Laravel over HTTPS. Clicking through the console is the best way to understand what each piece is. Once it feels familiar, the reference [AWS Lambda series](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) shows how to do all of this from the command line and automate it with infrastructure as code, so you never have to click it again. ::note{label="Build your first Lambda"} The final part of a two-part beginner walkthrough. Previous: [Build a QR Code Lambda and Call It From Laravel](https://franktheprogrammer.com/articles/build-a-qr-code-lambda-and-call-it-from-laravel) :: # Filtering Arrays Without Using Loops in PHP Have you ever had the need to filter through an existing array and create a new array based on your filter for your application to process? I'm sure the majority of you have, and personally I find it a bit tedious having to create a loop each time I need to filter through the data I need. PHP has a built-in function called array\_filter that allows you to filter through your arrays without the need for a loop. Personally, this approach feels much cleaner to me and simpler to comprehend. **Example** Let's say we have an array of People, with different names and ages. For our application though, we'd want to get a list of all People age 21 or above. First let's create our Person class. ```php name = $name; $this->age = $age; } public function name() : string { return $this->name; } public function age() : int { return $this->age; } } ``` Now let's create our array of people. ```php age() > 20) { $peopleOfAge[] = $person; } } ``` Not very pretty right? Now, let's do this using our array\_filter function. ```php age() > 20; }); ``` This is a much cleaner approach and clearly explains its intent. # Flutter Version Management As you dive deep into Flutter you'll probably start to wonder how you can juggle multiple versions of Flutter per project. This is where FVM (Flutter Version Management) helps. Its purpose is managing different Flutter versions, allowing you to set specific versions for each of your projects. ## Setting Up FVM on macOS ### Installation On macOS, we'll start by getting Dart. If you don't have it yet, install it through Homebrew: ```bash brew tap dart-lang/dart brew install dart ``` Now let's install FVM. You can do so in a couple of different ways. **Option 1**: Using Dart ```bash dart pub global activate fvm ``` **Option 2**: Directly via Homebrew ```bash brew install fvm ``` ### Exploring Available Versions You can check the available Flutter versions with the following command: ```bash fvm releases ``` ### Installing a Specific Version You can install specific versions of Flutter using the following command: ```bash fvm install ``` ### Switching Between Versions Swapping between Flutter versions is as easy as follows: ```bash fvm use ``` When you run the `fvm use ` command within your project directory, here's what happens: - FVM creates a symbolic link in your project directory to the specified Flutter version you want to use. This symbolic link is directed to the `.fvm/flutter_sdk` directory. - The `fvm use` command adjusts the Flutter SDK path for your project to point to the selected version. - With the selected Flutter version now active, any version-specific dependencies in your project will now align with your set version of Flutter, ensuring compatibility as you develop. ### Setting a Global Default Version You can set a global default version of Flutter on your system with the following command: ```bash fvm global ``` And that's it. This will allow you to now switch between versions easily as you work on projects. ::note{label="From 2026"} FVM is still the common way to pin Flutter versions per project. `fvm use ` and `.fvm/flutter_sdk` continue to work as described here. :: # Getting More Out of Claude Code: Prompting and Token Economy For months my Claude Code setup was one reflex: whatever the newest Opus is, pinned to max effort, on everything. Renaming a variable? Max effort. Reading a config file? Max effort. It felt responsible. Then I read the docs and watched my own `/usage` numbers, and changed my mind. Max effort is a great tool and a terrible default. This is what I have learned about getting more out of Claude Code: prompting it for fewer wrong turns, keeping the context window healthy, and choosing models and effort so you spend tokens where they pay off. One framing first. The scarce resource is throughput, how much work you get done before you hit your plan's usage and rate limits. Waste tokens and you just hit that wall sooner. ## It is a context window, not a chat The most useful mental model: you are not having a conversation, you are managing a context window. Everything Claude knows right now shares one finite budget of tokens, the conversation history, every file it has read, every command's output, your CLAUDE.md, loaded skills, and the system prompt. That budget is large but not infinite, and what matters is what happens as it fills. Claude Code auto-compacts, summarizing old turns and dropping stale output. That keeps the session alive but loses detail: the instruction from your third message can be gone by the fortieth. It is why long sessions slowly get dumber, not because the model got worse but because the early signal got compacted into noise. Almost everything below follows from this. Good prompting puts the right things in the window; good token economy keeps the wrong things out. ## Prompt for fewer wrong turns What you get back is mostly decided before Claude writes a line. Clever phrasing matters less than it used to; structure matters more, how specific you are, what context you hand over, and whether Claude can tell when it succeeded. **Be specific, up front.** Vague requests get vague work. Compare: ```text Fix the login bug. ``` ```text Logins fail for users whose email changed since signup. I think the session token still keys off the old address. Look in src/auth/session.ts, write a failing test that reproduces it, then fix it. Do not touch the OAuth flow. ``` The second names the symptom, points at the file, sets a boundary, and bakes in a way to know it worked. One pass instead of three. **Name the off-limits zones.** Say which files Claude should not touch, and have it run `git diff` at the end to confirm. What is out of bounds is often more useful than what to do. **Show an example of the output you want.** A small sample of the format steers Claude better than a paragraph describing it. **Say what to do, not what to avoid.** "Write in plain paragraphs" works better than "do not use bullet points." **Give it something to verify against.** The big one. A failing test, expected output, a screenshot, anything Claude can check itself against cuts the back-and-forth. A model that can watch a test go green does not need you as its test harness. ## Plan before you code For anything bigger than a quick edit, separate thinking from doing. Plan mode (Shift+Tab) lets Claude read the code and propose an approach without touching a file; you correct the misunderstanding while it is cheap, then let it build against a plan you agree on. The explore, plan, code, commit loop catches the expensive architectural mistakes before they become a diff you have to unwind. Two habits make it work. Read the plan before approving it, do not rubber-stamp it. And interrupt early: press Esc the moment Claude heads the wrong way instead of watching a doomed attempt finish. When a session gets truly tangled, stop nursing it, `/clear` and restart with a tighter prompt. That is almost always faster than arguing your way out, and it costs nothing. ## CLAUDE.md and memory, done right CLAUDE.md is the context you do not want to retype: build commands, architecture, conventions, the facts Claude should always know. It loads every session, so keep it lean, a couple hundred lines at most. Past a certain size it both eats context and gets followed less, so a bloated CLAUDE.md is worse than a short one. - **Facts, not procedures.** "Use 2-space indentation" belongs here; a ten-step deploy runbook is a job for a skill, which loads only when needed. - **Know the layers.** Project file for team standards, `~/.claude/` for personal preferences across projects, a gitignored local file for machine-specific notes. - **Scope rules to paths.** Instructions for one corner of the codebase can live in `.claude/rules/` so they load only when Claude touches matching files. - **Tell compaction what to keep.** A line in CLAUDE.md can say what survives summarization, so code and test output stay while small talk goes. Skills take this further. A skill is a folder: a short `SKILL.md` that says what it is for, next to a `references/` directory it links into. Claude loads only the one-line description at startup, the `SKILL.md` body when the skill triggers, and a reference file only when it needs that detail. A skill for publishing articles, say, might pair a lean `SKILL.md` with a `references/frontmatter.md` and friends. It is progressive disclosure all the way down, the best way to give Claude deep knowledge without it camping in context all session. ## Subagents keep the main context clean Some work is necessary but messy: grepping a big dependency, reading a wall of test output, researching an API across a dozen pages. You want the conclusion, not the firehose sitting in your main context for the rest of the session. That is what subagents are for. A subagent runs in its own context window, does the noisy work, and returns only a summary; the thousand lines of log never touch your window, the three-sentence finding does. Be clear-eyed, though: a subagent is not free. Each one boots with its own instructions and re-reads files to orient. For genuinely verbose, separable work that overhead pays off and can even lower total usage, since the firehose lives and dies in the subagent instead of re-inflating your context every turn. For a trivial task it is pure waste. Delegate when the work is big and noisy, not reflexively, and run a lighter model like Sonnet or Haiku for the grunt work. ## Models and effort: the max-effort question Here is the part I got wrong for months: Claude Code gives you two independent dials, and I treated one as fixed. The first is the **model**. Mid-2026 the lineup is Fable 5 for long autonomous sessions, Opus 4.8 for complex reasoning and architecture, Sonnet 4.6 as the everyday workhorse, and Haiku 4.5 for fast, simple tasks. Opus draws down your usage faster than Sonnet per token and earns it on hard problems; on a one-line change it does not. The second is **effort**, which on Opus 4.8 runs low, medium, high, xhigh, and max. Effort sets how much the model thinks before answering, and thinking tokens count as output, the heaviest kind. The detail worth tattooing somewhere: the default on Opus 4.8 is **high**, not max. Max removes the thinking cap and applies to the current session only, and the docs are blunt, it "can show diminishing returns and is prone to overthinking. Test before adopting broadly." So my max-effort-on-everything habit spent deep deliberation on tasks that needed none, sometimes reasoning worse by talking itself in circles. The fix is not a clever prompt, it is the dial: - **High by default.** It is the default for a reason, right for most real work. - **Max for genuinely hard problems.** Gnarly architecture, a subtle bug, anything ambiguous enough to reward deliberation. Deliberately, not reflexively. - **Medium or low for the routine.** Small refactors, mechanical edits, lookups. The thinking budget is wasted there. - **Sonnet for most daily coding,** Opus for decisions that turn on raw capability, Haiku or a subagent for grunt work. A third, separate dial: `/fast`. Fast mode is not a dumber model, it is the same Opus at lower latency, drawing down usage faster for the speed. It is independent of effort. Worth it for live debugging where you wait on each turn, wasteful on background work nobody is watching. ## Spending tokens well With the dials set sensibly, the rest is hygiene. **Watch the window.** `/context` shows what is eating your space; `/usage` shows what the session used and where. The numbers surprise you the first time. **Compact and clear on purpose.** `/compact` summarizes a long but still-relevant session, and you can focus it, for example to keep the API changes you are working on. `/clear` wipes the slate for unrelated work. Clearing is not losing progress; stale context taxes every later message. **Let caching work.** Claude Code auto-caches the stable parts of your context, the system prompt, tools, CLAUDE.md, the early turns, so they are not reprocessed each request; a cached read costs about a tenth of a fresh one. Editing earlier content invalidates everything after it, one more reason to keep CLAUDE.md stable and append rather than rewrite. **Do not dump the haystack to find the needle.** Reading a 50,000-line log to find three errors is the classic budget-killer. Grep first, then let Claude read the matches. **Prefer surgical edits and lean output.** Claude already edits with targeted diffs, which is good for tokens. Asking for less explanation helps on the output side, though it is a smaller lever than the internet thinks. ## Myths vs. facts Plenty of token-saving advice circulates. Sorted: **"Run Opus at max effort on everything."** *Mostly false.* High is the default for a reason; max is a precision tool that shows diminishing returns elsewhere. Always-max burns more and sometimes reasons worse. **"A million-token context window means you can stop worrying about context."** *False.* More runway, not a license to ignore it. Quality degrades from compaction well before any hard ceiling. **"This third-party tool cuts your tokens 80 to 90 percent."** *Marketing, mostly.* Unverified numbers. The real built-in levers are prompt caching and subagents, reach for those before someone's miracle wrapper. **"Running `/clear` throws away your work."** *False.* It resets the conversation context only; your files and git history are untouched. **"Fast mode is just a faster version of the model."** *Misleading.* Same model, faster, drawing down usage quicker, and a separate dial from effort. Use it when latency matters, not to stretch your limits. **"Magic words like 'think harder' make it reason more."** *Almost entirely false.* Claude Code recognizes one keyword, `ultrathink`, for a deeper-reasoning turn; "think hard" and the rest are ordinary text. And `ultrathink` spends *more* tokens, not fewer. ### Do token-saving mega-prompts work? A tempting idea: paste one fixed block into your custom instructions and permanently cut your spend. A representative version: ```text Launch subagents. Output only the modified or requested code block. Do not provide line-by-line explanations, setup guides, introductory or concluding remarks, or markdown commentary unless explicitly asked. Adopt an ultra-concise, high-density communication style. ``` Half useful, half misunderstanding. **"Launch subagents."** Cargo cult. There is a real feature nearby, Claude Code can orchestrate dynamic workflows, but you enable that with the `ultracode` setting from `/effort`, not by typing the words. As a blanket habit subagents cost tokens rather than save them: each boots with its own instructions and re-reads files, so firing one off for a two-line change is pure overhead. Delegate for genuinely noisy work, not as an incantation. **"Output only the modified code block."** The legitimate half. Output tokens count several times heavier than input on Opus, so trimming explanation you did not need is a real saving. Just know it is a trim, not the near-zero these prompts promise, and a permanent "only ever output code, never explain" rule hurts the moment you want Claude to plan or teach. **"Ultra-concise style forces the thinking to compress."** Here the reasoning breaks. The complaint is about thinking tokens, but a style instruction controls the output, not the thinking; they are different pools on different dials. The real fix for thinking spend is the effort dial, drop to high or medium, or move routine work to Sonnet. You can even tell Claude to think less directly. A communication-style instruction only trims the reply you read. So the honest version is small: ask for concise output when you do not need the explanation. The rest is a no-op or the wrong tool. What actually stops Opus Max from eating your limits is not a prompt, it is turning effort down and picking the right model. ::note{label="The short version"} Two dials, used on purpose, will do more than any copy-paste prompt: pick the model for the task, and treat max effort as a tool for hard problems rather than a default. Keep the context window clean, let caching work, and check your own `/usage` numbers. The current details live in the official docs at [code.claude.com/docs](https://code.claude.com/docs){rel=""nofollow""}. :: # Git Flow vs GitHub Flow: Choosing a Branching Strategy for Your Team There's no single "right" way to branch your code. Git Flow, GitHub Flow, and trunk-based development are all popular, all battle tested, and all used by teams shipping great software every day. The trick isn't picking the "best" one, it's picking the one that matches how your team actually releases. In this article we'll compare the two big names, see where trunk-based development fits in, and sort out how to handle versioned releases along with the fixes that inevitably follow. ## Two Ways to Think About Branching It helps to picture branching strategies on a spectrum. On one end you have Git Flow, which is structured and ceremony-heavy, built around scheduled, versioned releases. On the other end you have GitHub Flow, which is about as simple as it gets: one long-lived branch that's always ready to deploy. Most teams land somewhere along that line. So before you copy a workflow just because a big company uses it, ask yourself one question: how often do we ship, and do we need to support more than one released version at a time? Your answer points you to the right end of the spectrum. ## Git Flow: Structure for Versioned Releases Git Flow has been around since 2010, and it's the most formal of the bunch. It uses two long-lived branches plus three kinds of supporting branches. - `main` (or `master`) is what's in production. - `develop` is the integration branch where finished work collects. - `feature/*` branches come off `develop` for each new piece of work. - `release/*` branches freeze `develop` so you can harden a version before shipping it. - `hotfix/*` branches come off `main` to patch production in a hurry. I won't rehash every command here, since I already walked through the full setup in [Improve your Git workflow with Git Flow](https://franktheprogrammer.com/articles/improve-your-git-workflow-with-git-flow). What matters for this comparison is the shape: work flows from feature, to develop, to release, to main, with versions tagged along the way. That structure really shines when you ship versioned software on a schedule. Think desktop apps, libraries, mobile releases, or anything where multiple versions live in the wild at once and need separate maintenance. Now for the honest caveat. Even Vincent Driessen, who created Git Flow, later added a note to his original post saying that if your team practices continuous delivery, you should probably reach for something simpler. Atlassian these days describes it as a legacy workflow. None of that makes it wrong, it just means the extra branches are overhead you only want to pay for when versioned releases actually earn it. ## GitHub Flow: Keep Main Deployable GitHub Flow throws out almost all of that ceremony. It has exactly one rule: anything in `main` is always deployable. The loop is short: 1. Branch off `main` for your change. 2. Open a pull request and let your team review it. 3. Merge back into `main`. 4. Deploy. ```bash git checkout -b add-search-filters # do the work, commit git push -u origin add-search-filters # open a PR, get a review, merge, deploy ``` There's no `develop`, no `release` branches, and no waiting for a release window. Branches are short-lived, usually open for hours or a day or two rather than weeks. Because `main` is always shippable, you can deploy the moment a PR lands. I walk through the whole loop, commands and all, in [GitHub Flow: Keep Your Main Branch Deployable](https://franktheprogrammer.com/articles/github-flow-keep-your-main-branch-deployable). This is a great fit for web apps and services, where there's really only one "version" running: whatever is live right now. It's also friendlier to small teams and to continuous integration, since you're constantly merging small changes instead of nursing a giant `develop` branch toward a release. Where it strains is exactly where Git Flow is strong. If you need to support several released versions at once, or you do heavy release hardening, GitHub Flow's single main branch starts to feel a little thin. ## A Quick Word on Trunk-Based Development If GitHub Flow felt minimal, trunk-based development goes one step further. Everyone integrates into a single trunk (your `main`) constantly, with branches that live for hours, not days. Anything that isn't ready for users hides behind a feature flag instead of sitting on a long-lived branch. This is the workflow most associated with modern continuous delivery and high-performing DevOps teams, because it keeps merges tiny and integration pain close to zero. It does lean on solid automated tests and feature flags to stay safe, so it asks more of your tooling than it does of your branching. You can think of GitHub Flow as the comfortable middle ground: simpler than Git Flow, but with pull requests and short feature branches still giving you a natural review checkpoint. ## Fixed Version Releases Whichever flow you choose, the moment you start shipping versions you'll want a sane way to number and mark them. That's what semantic versioning is for. A version looks like `MAJOR.MINOR.PATCH`: - **MAJOR** when you make a breaking change. - **MINOR** when you add functionality in a backward-compatible way. - **PATCH** when you ship a backward-compatible bug fix. So `2.4.1` is the second major version, its fourth feature release, with one patch on top. Tag each release so you can always check out exactly what you shipped. ```bash git tag -a v2.4.1 -m "Release 2.4.1" git push origin v2.4.1 ``` In Git Flow, this is where `release/*` branches pay off. You cut `release/2.4.0` off `develop`, fix only the release blockers there, then merge it into `main`, tag it, and merge back into `develop` so nothing gets lost. Pair your tags with a short changelog and your team (along with your users) always knows what changed and when. ## Hotfixes, Warmfixes, and Coldfixes Not every fix is an emergency, and treating them all the same is how you either move too slowly or ship recklessly. It helps to think of fixes on a temperature scale. **Hotfix.** Production is on fire: a crash, a security hole, a checkout that won't check out. You branch straight off `main`, make the smallest change that works, and ship it now. In Git Flow that's a `hotfix/*` branch off `main`, merged back into both `main` and `develop` and tagged with a new patch version. In GitHub Flow it's just a normal short branch off `main`, except it jumps the queue. ```bash git checkout -b hotfix/login-crash main # smallest possible fix, commit git checkout main git merge hotfix/login-crash git tag -a v2.4.2 -m "Hotfix: login crash" ``` **Warmfix.** This one is informal, you won't find it in any spec, but it's a handy label for the middle of the scale. A warmfix is important but not on fire. You don't want to wait a full release cycle, but you also don't need to bypass your normal checks. In practice it rides an expedited path: a patch release cut soon, or folded into a `release/*` branch that's already in flight, rather than going straight to production. **Coldfix.** A routine fix with no urgency. It goes through the front door like any other change: into `develop` (or onto `main` via a PR in GitHub Flow), and out with the next scheduled release. The point isn't the vocabulary, it's having a shared sense of how fast something needs to move and what you can safely skip to get it there. When you're coordinating fixes across a team, it also helps to keep your local copy in sync with everyone else's, which I covered in [tracking a remote branch for changes](https://franktheprogrammer.com/articles/git-tracking-a-remote-branch-upstream-for-changes). ## So Which One Should You Use? Here's how I'd decide. If you're a small team shipping a web app or service and you deploy often, start with GitHub Flow, then graduate to trunk-based development once your tests and feature flags can support it. If you ship versioned software on a schedule and have to support multiple releases in the wild, Git Flow's extra structure earns its keep. But honestly, the specific convention matters less than picking one and sticking to it. A team that agrees on how branches are named, when things merge, and how releases get tagged will outship a team that's improvising every time, no matter which workflow is on the whiteboard. So pick the one that fits how you actually release, write it down, and get everyone using it. That consistency is the real win, and that's all there really is to it. # Git: Tracking a Remote Branch for Changes Sometimes you need to track a remote branch for changes, typically the original project you forked from, conventionally named *upstream*. Say you are on a team where you forked a project and have your own copy. Before you open a pull request, you want to pull in any changes from the originating repository so your work sits on top of the latest code. Here is how to set that up and keep it in sync. ## Two different things called "upstream" The word *upstream* gets used for two related but distinct things, and mixing them up is where people get confused. 1. **The upstream remote.** A named remote (conventionally `upstream`) pointing at the original repository you forked, so you can fetch the project's new commits. Your own fork is usually the `origin` remote. 2. **A branch's upstream tracking ref.** The link between one local branch and the specific remote branch it pulls from and pushes to. Your local `main` "tracks" `origin/main`. This relationship is what powers a bare `git pull` and `git push`, and what produces the `Your branch is up to date with 'origin/main'` line in `git status`. This post covers both. First the fork scenario, then branch tracking at the end. ## Add the upstream remote Start by checking what remotes you already have. Right after forking and cloning, you will usually see only `origin`, which is your fork. ```bash git remote -v ``` Add the original repository as a second remote named `upstream`. ```bash git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPO.git ``` > Swap `ORIGINAL_OWNER` and `ORIGINAL_REPO` for the username and repository name from the original project's URL, the one you forked. Confirm it is configured. You should now see both `origin` (your fork) and `upstream` (the original), each with a fetch and a push entry. ```bash git remote -v ``` ## Actually sync: fetch, merge, push Adding the remote is only half the job. The whole point is to pull the upstream project's new commits into your fork. That is three steps: fetch, merge, push. ```bash git fetch upstream # download upstream's commits and refs (merges nothing yet) git checkout main # be on your local default branch git merge upstream/main # bring upstream's main into your local main git push # update YOUR fork (origin) on GitHub ``` A couple of things worth calling out. `git fetch upstream` downloads everything but changes none of your branches; it just makes `upstream/main` available locally. The final `git push` matters: syncing only updates your local clone, so without it your fork on GitHub is still behind. ## Merge or rebase? I used `git merge` above, which is the safe default. The alternative is to rebase your branch on top of upstream instead. ```bash git rebase upstream/main ``` Merging is non-destructive, but it adds a merge commit each time you sync. Rebasing replays your commits on top of upstream for a cleaner, linear history, at the cost of rewriting commit hashes. The Golden Rule of Rebasing applies: never rebase a branch that other people have already pulled. For keeping a personal fork's `main` in step with upstream, either works. If you keep your `main` as a clean mirror of upstream and do all your real work on feature branches, these syncs fast-forward and the whole question goes away. ## The quick way: gh CLI and the Sync fork button If your fork lives on GitHub, you often do not need the manual commands at all. The GitHub CLI can sync the fork for you. ```bash gh repo sync # sync your local clone from its parent gh repo sync OWNER/YOUR_FORK # sync the fork on GitHub from upstream ``` GitHub's web UI has the same thing: on your fork's page, the "Sync fork" dropdown shows how far behind you are and updates your branch with one click. These are convenient, but knowing the fetch and merge underneath is what saves you when a sync hits a conflict. ## Bonus: tracking a remote branch for real The fork case is one kind of "tracking." The other is setting a branch's upstream tracking ref, so plain `git pull` and `git push` know where to go without you naming a remote and branch every time. When you first push a new branch, `-u` (short for `--set-upstream`) creates the branch on the remote and wires up tracking in one go. ```bash git push -u origin my-feature ``` For a branch that already exists, you can set or change its upstream after the fact. ```bash git branch --set-upstream-to=origin/main ``` To see what each branch is tracking, ask for the verbose listing. It shows the tracked branch and how far ahead or behind you are. ```bash git branch -vv ``` That tracking link is exactly what `git status` reads when it tells you `Your branch is up to date with 'origin/main'`, and it is what lets a bare `git pull` know which remote branch to fetch and merge. ## Keep your main clean One habit makes all of this painless: never commit directly to the branch you sync. If your local `main` only ever moves by syncing from upstream, every sync fast-forwards cleanly. The moment you commit your own work onto `main`, easy fast-forwards turn into merge conflicts. Do your work on feature branches off a clean `main`, and keep `main` as a mirror of the source of truth. That is the whole loop: add the upstream remote once, then fetch and merge (or let `gh` do it) whenever you need the latest, and push to update your fork. When you are coordinating this across a team, it pairs nicely with a shared branching strategy, which I cover in [Git Flow vs GitHub Flow](https://franktheprogrammer.com/articles/git-flow-vs-github-flow-choosing-a-branching-strategy). # GitHub Flow: Keep Your Main Branch Deployable If [Git Flow](https://franktheprogrammer.com/articles/improve-your-git-workflow-with-git-flow) is the structured, ceremony-heavy end of the branching spectrum, GitHub Flow is the opposite. It throws out the long-lived branches and release windows and runs on one rule: anything in `main` is deployable. Everything else follows from that. If you want the head to head, I wrote a [Git Flow vs GitHub Flow comparison](https://franktheprogrammer.com/articles/git-flow-vs-github-flow-choosing-a-branching-strategy); this post is the hands-on deep dive into GitHub Flow itself. ## The one rule that drives everything GitHub Flow has exactly one promise: whatever is on `main` right now could be deployed to production this second without a second thought. That single constraint is what makes the rest of the workflow so light. There is no `develop` branch collecting work, no `release` branch to harden, no version window to wait for. There is `main`, and there are short-lived branches that become `main` once they pass review. The trade you are making here is an honest one. You get speed and simplicity, and you give up the ability to maintain several released versions at once. For a web app or a service, where the only version that matters is whatever is live, that is a great deal. For shrink-wrapped software that has to support v1 and v2 in the field, it is not, and that is where Git Flow earns its structure instead. ## The loop, step by step GitHub's own docs describe GitHub Flow as a lightweight, branch-based workflow, and lay it out in six steps: 1. **Create a branch** off `main` with a short, descriptive name. 2. **Make changes** as small, complete commits, pushing as you go. 3. **Open a pull request** describing what changed and why. 4. **Address review feedback** by pushing more commits to the same branch. 5. **Merge** the pull request once it is approved and checks pass. 6. **Delete the branch**, since its work now lives in `main`. You will notice "deploy" is not its own numbered step in the modern docs. That is because deployability is baked into the philosophy rather than bolted on at the end. Scott Chacon's original 2011 description of GitHub Flow was more explicit about it: once something is merged into `main`, you can and should deploy it immediately. Either way, the mental model is the same: branches are temporary, `main` is always ready, and shipping is the easy part. ## The command walkthrough Here is one full trip around the loop in the terminal. I will use the [GitHub CLI](https://cli.github.com/){rel=""nofollow""} (`gh`) for the pull request bits, but you can do all of it from the web UI too. ```bash # Start from an up-to-date main git checkout main git pull origin main # Create a descriptively named branch for the change git switch -c increase-test-timeout # Make an isolated, complete change and commit it git add . git commit -m "Increase CI timeout so flaky integration tests stop failing" # Push the branch and set it to track the remote git push -u origin increase-test-timeout # Open a pull request gh pr create --title "Increase test timeout" \ --body "Bumps the CI timeout so flaky integration tests stop failing. Closes #123" ``` After review, merge and clean up in one command. ```bash gh pr merge --squash --delete-branch ``` That is the entire workflow. Branch, commit, push, PR, merge, delete. Because `main` is shippable the moment that PR lands, you deploy right after. ## Branch names and commits that do not make you wince Two small habits keep GitHub Flow pleasant. First, name branches for what they do. `increase-test-timeout` and `add-code-of-conduct` tell your teammates what is in flight at a glance; `fix` and `wip` do not. Second, keep each commit to a single, complete change with a message that says what it does. Small commits are easy to review and trivial to revert when something slips through. You are not writing a novel, you are leaving a trail the next person (often future you) can follow. ## Pull requests are the checkpoint The pull request is the heart of GitHub Flow, and it is more than a merge button. It is where the change gets explained, reviewed, and discussed. A good PR description says what changed and what problem it solves, links the issue it closes with a keyword like `Closes #123` so the issue closes automatically on merge, and pulls in the right reviewers with an @mention or a review request. This is also where your automation does its job. Branch protection rules can require a passing CI run and at least one approval before the merge button lights up, which is what lets you trust the "`main` is always deployable" promise in the first place. The PR is your quality gate; treat it like one. ## Keeping your branch fresh Because branches are short-lived, they rarely drift far from `main`. When one does, you bring `main` into it before merging. ```bash git fetch origin git merge origin/main # safe on shared branches # or, on a branch only you have touched: git rebase origin/main # cleaner, linear history ``` Merge is the safe choice on anything someone else might have pulled. Rebase gives you tidy history but rewrites commits, so save it for branches that are yours alone. Resolve any conflicts here, on your branch, not on `main`. ## Deploy on merge, or deploy before merge? There is one honest debate here. Chacon's original flow says merge into `main`, then deploy immediately. Plenty of modern teams flip that: they deploy the branch to a staging or preview environment and validate it live *before* merging, so `main` only ever receives code that has already proven itself in production-like conditions. GitHub's own branch-deploy style works this way. Neither is wrong. Deploy-on-merge is simpler and keeps the loop tight. Deploy-before-merge gives you a real safety net at the cost of more tooling. Pick based on how much you trust your tests and how expensive a bad deploy is for you. ## Hotfixes are just normal branches Here is the payoff of all this simplicity. In Git Flow, an emergency fix means a dedicated `hotfix/*` branch off `main`, merged back into two places and tagged. In GitHub Flow, a hotfix is just a normal short branch off `main` that jumps to the front of the review queue. ```bash git switch -c hotfix-login-null-check main git commit -am "Add null check on the login handler" git push -u origin hotfix-login-null-check gh pr create --fill && gh pr merge --squash --delete-branch ``` Same loop, same `main`, just faster. There is nothing special to learn because there is nothing special about it. ## Where it fits, and where it strains GitHub Flow is a great fit for web apps, services, and SaaS, anywhere there is really only one live version and you deploy often. It is friendly to continuous integration because you are constantly merging small changes instead of nursing a giant branch toward a release. Two places it strains. It has no clean home for maintaining multiple released versions at once, so if customers stay on old releases you will want Git Flow's release branches instead. And "`main` is always deployable" is only true if your tests and pipeline actually enforce it; without solid CI, that promise is wishful thinking. GitHub Flow has a close relative, trunk-based development, which goes one step further: everyone integrates into `main` constantly, branches live for hours, and anything unfinished hides behind a feature flag. You can think of GitHub Flow as the comfortable middle ground, trunk-based development with pull requests and short feature branches still giving you a natural review checkpoint. ## Wrapping up GitHub Flow really does come down to that one rule: keep `main` deployable, and let short branches and pull requests carry everything else. If you ship a web app and deploy continuously, start here. If you ship versioned software with multiple releases to support, read the [Git Flow deep dive](https://franktheprogrammer.com/articles/improve-your-git-workflow-with-git-flow) instead, and if you are still deciding between them, my [comparison article](https://franktheprogrammer.com/articles/git-flow-vs-github-flow-choosing-a-branching-strategy) lays out exactly how to choose. # Give Your Lambda an HTTP Front Door In [part one](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) we packaged a Python thumbnailer as a container image, pushed it to ECR, and invoked it directly. Direct invocation is perfect for internal tools and event-driven work, but it needs AWS credentials and request signing on every call, so it is not how a browser, a webhook, or a third party reaches your function. For that you want a plain HTTPS endpoint. Lambda gives you two front doors, and knowing which to pick (and when you need neither) is most of the battle. ## You already have one way in: direct invocation It is easy to forget this, so say it out front: API Gateway is just one of many ways to trigger a Lambda. You can invoke it directly with the SDK or CLI, and in production it is far more often triggered by events: an S3 upload, an SQS message, an EventBridge schedule, a DynamoDB stream. None of those involve API Gateway at all. Direct invocation shines when the caller is your own backend and already holds AWS credentials. From a server-side app, signing the request is automatic and there is no public surface to secure. The moment the caller is a browser or someone else's system, though, you want HTTP. ## The simplest HTTP: Lambda function URLs A function URL is a dedicated HTTPS endpoint bolted straight onto one function, with no API Gateway in the picture. You get a permanent address shaped like `https://.lambda-url..on.aws`. The quickest way to add one: ```bash aws lambda create-function-url-config \ --function-name thumbnailer \ --auth-type NONE ``` `AuthType` is either `NONE` (anyone with the URL can call it, good for public endpoints and webhooks) or `AWS_IAM` (callers must sign requests with IAM credentials). CORS is built in, and for repeatable infrastructure you would define the URL in CloudFormation rather than the CLI: ```yaml Type: AWS::Lambda::Url Properties: AuthType: NONE TargetFunctionArn: !Ref Thumbnailer Cors: AllowOrigins: ["*"] AllowMethods: ["POST"] ``` The lovely part: function URLs deliver events in the **same payload format 2.0** as API Gateway HTTP APIs, so the handler code is identical whether you front it with a function URL or a full API. The limits are real, though. You get one function and one route, no custom domain, no API keys or usage plans, and no AWS WAF. Throttling exists only indirectly: you cap concurrency with reserved concurrency, and your ceiling is roughly ten requests per second for each unit of it. A function URL is the right call for a single endpoint, a webhook receiver, or an internal service. For a real API you graduate to API Gateway. ## What API Gateway actually buys you API Gateway sits in front of one or many Lambdas and adds everything an API needs around the raw function. It comes in two flavors, and the choice mostly comes down to which features you need. **HTTP API** is the newer, cheaper, lower-latency option, and the right default for a straightforward Lambda proxy. **REST API** is the older, fuller, pricier one. Here is the split that matters in practice: - **Both HTTP and REST give you:** routing across many paths, methods, and functions; stages; custom domain names with managed TLS; CORS; authorizers for IAM, Amazon Cognito, and custom Lambda logic; mutual TLS; CloudWatch metrics and access logs. - **HTTP API adds:** native JWT authorizers and automatic deployments, at a noticeably lower price. - **REST API alone gives you:** API keys and usage plans, per-client rate limiting, request validation, AWS WAF integration, response caching, edge-optimized and private endpoints, request body transformation, and X-Ray tracing. So the decision guide reads like this: - **Invoke directly** (SDK or CLI) when the caller is your own credentialed backend or an AWS event source. - **Function URL** when you need the simplest possible public HTTPS for a single function, and you do not need keys, custom domains, or WAF. - **HTTP API** for a real web API with routes, a custom domain, and JWT or Cognito auth, at the lowest cost. This covers most applications. - **REST API** when you specifically need API keys and usage plans, request validation, WAF, caching, or private endpoints. For the thumbnailer, an HTTP API is the sweet spot, so that is what we will build. ## Build the HTTP API with SAM You could click this together in the console, but the AWS Serverless Application Model (SAM) lets you declare the function, the API, and the permissions in one file, and it doubles as a local test rig. A `template.yaml` for our image-based function: ```yaml AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Resources: Thumbnailer: Type: AWS::Serverless::Function Properties: PackageType: Image Architectures: [arm64] MemorySize: 512 Timeout: 30 Events: Resize: Type: HttpApi Properties: Path: /thumbnail Method: POST Metadata: Dockerfile: Dockerfile DockerContext: . DockerTag: latest Outputs: ApiUrl: Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/thumbnail" ``` The `HttpApi` event creates an HTTP API for you and (this is the quietly useful bit) wires up the permission that lets API Gateway invoke the function. Building and deploying is two commands: ```bash sam build sam deploy --guided ``` On the first guided deploy, SAM offers to create and manage a private ECR repository for your image automatically, so you can skip the manual `aws ecr` dance from part one entirely. After that, `sam deploy` redeploys with the settings it saved. ## Handle the request, and return a real image Over HTTP, the function no longer receives a bare dictionary. Payload format 2.0 wraps the request: the JSON you posted is in `event["body"]`, the method is at `event["requestContext"]["http"]["method"]`, and there are `rawPath` and `queryStringParameters` fields too. The neat trick is to make the handler work for both an HTTP request and a direct invoke: ```python import base64 import io import json from urllib.request import urlopen from PIL import Image def load_image_bytes(params): if "image_base64" in params: return base64.b64decode(params["image_base64"]) if "image_url" in params: with urlopen(params["image_url"]) as response: return response.read() raise ValueError("Provide image_base64 or image_url") def handler(event, context): over_http = "requestContext" in event or "body" in event # An HTTP request wraps the payload in a body; a direct invoke passes it as-is. if over_http: body = event.get("body") or "{}" if event.get("isBase64Encoded"): body = base64.b64decode(body).decode() params = json.loads(body) else: params = event width = int(params.get("width", 200)) image = Image.open(io.BytesIO(load_image_bytes(params))) image.thumbnail((width, width)) buffer = io.BytesIO() image.save(buffer, format="PNG") thumbnail = base64.b64encode(buffer.getvalue()).decode() if over_http: # Return the PNG itself so a browser renders it directly. return { "statusCode": 200, "headers": {"Content-Type": "image/png"}, "isBase64Encoded": True, "body": thumbnail, } return {"width": image.width, "height": image.height, "thumbnail_base64": thumbnail} ``` Format 2.0 gives you two ways to respond. Return any JSON-serializable value and API Gateway auto-wraps it as a `200` with a JSON content type, which is great for plain data. Or return an explicit object with `statusCode`, `headers`, `body`, and `isBase64Encoded` for full control. To send back an actual PNG that a browser can show, you need that explicit form: base64-encode the bytes, set `isBase64Encoded` to `true`, and the HTTP API decodes it to binary on the way out. ## A note on permissions With the SAM `HttpApi` event above, the resource-based policy that allows API Gateway to call your function is created automatically. If you ever wire it by hand, that policy is what you are adding: ```bash aws lambda add-permission \ --function-name thumbnailer \ --statement-id apigateway-invoke \ --action lambda:InvokeFunction \ --principal apigateway.amazonaws.com \ --source-arn "arn:aws:execute-api:us-east-1:111122223333:abc123/*/POST/thumbnail" ``` The principal is always `apigateway.amazonaws.com` and the action is `lambda:InvokeFunction`. And because none of this cares how the function is packaged, a container-image Lambda is invoked by API Gateway exactly like a zip one. ## Test the whole API locally SAM can stand up a local API Gateway in front of your container, payload format 2.0 envelope and all: ```bash sam local start-api ``` It serves on `http://127.0.0.1:3000` by default. Because our handler returns the PNG with `isBase64Encoded`, you can save the response straight to a file and open it: ```bash curl -s -X POST http://127.0.0.1:3000/thumbnail \ -H "Content-Type: application/json" \ -d '{"image_url": "https://httpbin.org/image/jpeg", "width": 120}' \ --output thumb.png ``` For a single function with no HTTP layer, `sam local invoke -e event.json` runs it against an event file instead. Between this and the Runtime Interface Emulator from part one, you can exercise the whole thing before anything reaches AWS. ## Call it from a Laravel app Here is the payoff for the PHP crowd, and it mirrors the two front doors exactly. Install the SDK with `composer require aws/aws-sdk-php` (Laravel 13 wants PHP 8.3+). Through API Gateway it is just an HTTP request, so Laravel's HTTP client is all you need. Since the endpoint returns image bytes, you can pass them straight through to the browser: ```php use Illuminate\Support\Facades\Http; $response = Http::post('https://abc123.execute-api.us-east-1.amazonaws.com/thumbnail', [ 'image_url' => 'https://example.com/cat.jpg', 'width' => 120, ]); return response($response->body(), 200)->header('Content-Type', 'image/png'); ``` With no gateway at all, call the function directly with the AWS SDK for PHP. The handler detects the direct invoke and returns plain JSON, so you decode the base64 yourself: ```php use Aws\Lambda\LambdaClient; $lambda = new LambdaClient(['region' => 'us-east-1', 'version' => 'latest']); $result = $lambda->invoke([ 'FunctionName' => 'thumbnailer', 'Payload' => json_encode(['image_url' => 'https://example.com/cat.jpg', 'width' => 120]), ]); $data = json_decode($result->get('Payload')->getContents(), true); $png = base64_decode($data['thumbnail_base64']); ``` Which one to use? From a credentialed Laravel backend, the SDK invoke is direct and needs no public endpoint. When the caller is a browser, a mobile client, or a partner, put API Gateway or a function URL in front and call that. Credentials for the SDK come from the usual AWS chain: environment variables locally, and an IAM role on the server in production, so you never hard-code keys. You now have a function reachable over HTTP, with a clear sense of which door fits which job, tested locally and wired into a real app. One function behind one endpoint covers an enormous amount of ground. The next question is what happens when the work does not fit in one function: several steps, retries, things running in parallel. That is where Step Functions come in, and in part three we will see when they are worth it and when they are overkill. ::note{label="AWS Lambda series"} Part two of a hands-on series on running containerized Lambdas. Previous: [Package a Python Lambda as a Docker Image](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) · Next: [Orchestrating Lambdas with Step Functions](https://franktheprogrammer.com/articles/orchestrating-lambdas-with-step-functions) :: # Gulp Watch: Automate Your Gulp Tasks In this article, we'll go over how to automate your gulp tasks. If you're interested in how to configure and use gulp reference this article below. [Learn How To Configure and Use Gulp](https://franktheprogrammer.com/articles/configuring-gulp-on-a-new-project) Gulp watch is perfect for when you're editing project files since it allows you to not have to run the gulp command manually each time. We'll start off with a simple example by pulling in bootstrap-sass, and watching our SASS files merge and build as we make changes. ::note{label="From 2026"} This article uses Gulp 3's `gulp.watch(glob, ['taskname'])` signature. In Gulp 4 and later you pass a task function instead, e.g. `gulp.watch(glob, gulp.series('default'))`. Most current projects skip Gulp altogether and rely on [Vite](https://vite.dev/){rel=""nofollow""} for watching and rebuilding. :: ## Example First, let's go ahead and start a new project and pull in bootstrap-sass. ```bash yarn add bootstrap-sass ``` If you do not yet have a package.json file, make sure to run ```bash yarn init ``` We'll also need to add gulp. ```bash yarn add gulp ``` To compile our SASS to CSS we'll want to also include gulp-sass plugin. ```bash yarn add gulp-sass ``` Next, let's go ahead and setup our gulpfile.js and include our plugins along with some boiler plate from our previous article. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); gulp.task('default', function () { // Copy SASS files to new location gulp.src([ './node_modules/bootstrap-sass/assets/stylesheets/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*/*', ]).pipe(gulp.dest('./assets/sass/bootstrap/')); // Convert SASS to CSS gulp.src('./assets/sass/custom.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./css/')); }); ``` I went over in detail the process of building this file in my previous article [Configuring Gulp On A New Project](https://franktheprogrammer.com/articles/configuring-gulp-on-a-new-project). The key thing to note here, is that we're copying our bootstrap files to their new locations while also adding a file of our own called custom.scss, where we'll make our specific changes. I've included our custom.scss file as a reference as well, since we will be making our changes here for gulp.watch to check. ```scss @import 'bootstrap/bootstrap'; body { background-color: green; } ``` Now we need to add our watcher, this way we can simply just run gulp watch from the command line. We do this using the gulp.watch api as so. ```javascript gulp.watch('assets/sass/**/*.scss', [ 'default' ]); ``` Now every time you run gulp, it will start the watcher and look for changes within the files as you make them. You could essentially make a task for watch that would allow you to type gulp watch from the command line instead. For instance: ```javascript gulp.task('watch', function(){ gulp.watch('./assets/sass/*.scss', ['default']); }); ``` That's all there is to it. You can be as creative as you want with your watchers. I've also included my full gulpfile.js file as reference. ```javascript var gulp = require('gulp'); var sass = require('gulp-sass'); gulp.task('default', function () { // Copy SASS files to new location gulp.src([ './node_modules/bootstrap-sass/assets/stylesheets/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*', './node_modules/bootstrap-sass/assets/stylesheets/*/*/*', ]).pipe(gulp.dest('./assets/sass/bootstrap/')); // Convert SASS to CSS gulp.src('./assets/sass/custom.scss') .pipe(sass().on('error', sass.logError)) .pipe(gulp.dest('./css/')); }); gulp.task('watch', function(){ gulp.watch('./assets/sass/*.scss', ['default']); }); ``` # How to Build a Claude Code Plugin Once you have used Claude Code for a while you start to collect rituals: the way you phrase a commit, the checklist you paste before a release, the prompt you keep re-typing to summarize what changed. A plugin is how you bottle those rituals up and hand them to the rest of your team, or to yourself on a different machine, without copy and paste. A Claude Code plugin is just a folder of components that extends the tool: slash commands, skills, agents, hooks, and MCP servers, distributed through a marketplace so other people can install it with one line. In this article we are going to build a small but genuinely useful one called `git-flow`. It bundles two things: a `/commit` command that writes a Conventional Commit from your staged diff, and a `changelog` skill that Claude reaches for on its own when it is time to cut a release. By the end you will have published it through a marketplace and versioned it for an actual release. ## What lives inside a plugin Every plugin is a directory with one required file, the manifest at `.claude-plugin/plugin.json`. Everything else is optional and sits at the **root** of the plugin, not inside `.claude-plugin/`. That distinction trips people up, so be precise about it: only the manifest goes in `.claude-plugin/`. Your `commands/` and `skills/` folders live one level up. The plugin we are building looks like this: ```text git-flow/ ├── .claude-plugin/ │ └── plugin.json ├── commands/ │ └── commit.md └── skills/ └── changelog/ └── SKILL.md ``` The manifest itself only insists on one field, `name`, which must be kebab-case because it becomes the namespace for everything the plugin ships. The rest are metadata that show up in the `/plugin` interface and help people decide whether to trust your code: ```json { "name": "git-flow", "version": "0.1.0", "description": "Conventional Commits helper: a /commit command and an auto-invoked changelog skill.", "author": { "name": "Frank Perez", "email": "frank@fjp.io" }, "homepage": "https://github.com/frankperez87/git-flow", "license": "MIT" } ``` By default Claude Code discovers a `commands/` folder and a `skills/` folder automatically, so for a layout this conventional you do not need to point at them from the manifest. You only add `"commands"` or `"skills"` path keys when your files live somewhere non-standard. Note that setting those keys *replaces* the default folder rather than adding to it, so if you do override one, list every path you want scanned. ## Adding a slash command A command is the simplest component there is: a single Markdown file whose body is a prompt. The frontmatter describes the command, and the body is what Claude runs when you type the slash command. Create `commands/commit.md`: ```markdown --- description: Write a Conventional Commit from the staged diff argument-hint: [scope or intent hint] allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git diff:*), Bash(git commit:*) --- Look at the staged changes and write a single Conventional Commit. 1. Run `git diff --staged` to see what is actually staged. If nothing is staged, run `git status` and ask me what to include before continuing. 2. Infer the right type (feat, fix, docs, refactor, test, chore) and an optional scope from the files that changed. 3. Write a concise, imperative subject line under 72 characters. Add a short body only when the change genuinely needs explaining. 4. Treat anything in "$ARGUMENTS" as a hint about the scope or intent. 5. Show me the proposed message, and run `git commit` once I approve it. ``` Three things are doing the work here. The `description` is what shows up in the `/help` list and the plugin UI. The `allowed-tools` line scopes which Bash invocations the command may run without prompting you each time, which keeps a git helper from being able to touch anything else. And `$ARGUMENTS` is the placeholder that captures whatever you type after the command name, so `/git-flow:commit api error handling` drops `api error handling` straight into step four. That namespace prefix is automatic. Because the plugin is named `git-flow`, the command is invoked as `/git-flow:commit`, never just `/commit`. Namespacing is what lets two different plugins both ship a `commit` command without colliding. ## Adding a skill A command is something *you* invoke. A skill is something *Claude* invokes, on its own, when the situation calls for it. That single distinction is the reason a plugin usually wants both. You do not want to remember to type `/git-flow:changelog` at release time. You want to say "let's cut a release" and have the right capability load itself. Under the hood these two have actually converged: recent Claude Code versions merged custom commands into skills, so a `commands/foo.md` file and a `skills/foo/SKILL.md` both produce `/foo`, and a command is really just a skill that only you can trigger. Plugins still support both directories, which makes the split a handy way to organize: flat files in `commands/` for the things you invoke, folders in `skills/` for the things Claude should reach for on its own. Skills are directories rather than flat files, because they can carry supporting material alongside the instructions. The entry point is always `SKILL.md`. Create `skills/changelog/SKILL.md`: ```markdown --- name: changelog description: Assembles release notes from Conventional Commits. Use when the user asks to cut a release, write a changelog, or summarize what changed since the last tag. --- # Changelog Build a human-readable changelog from the Conventional Commits since the last release. ## Steps 1. Find the most recent tag with `git describe --tags --abbrev=0`. If there is no tag yet, use the full history. 2. Collect commit subjects with `git log ..HEAD --pretty=format:'%s'`. 3. Group them by Conventional Commit type: - `feat` becomes Added - `fix` becomes Fixed - `refactor` and `perf` become Changed - everything else becomes Other 4. Drop the type prefix from each subject and write it as a bullet, keeping the scope in parentheses when one is present. 5. Output Keep a Changelog style Markdown under a version heading, and suggest the next semantic version from the commit types: a `feat` implies a minor bump, a breaking change implies a major one. ``` The magic is in the `description`. Claude Code uses a progressive-disclosure model: at the start of a session it only loads each skill's name and one-line description, not the whole body. That is cheap, so a session can have dozens of skills available without flooding the context. When your request matches a description, Claude pulls the full `SKILL.md` into context and follows it. So the description is not flavor text, it is the trigger. Write it as "use when..." and name the phrases a user would actually say. This is also why the `changelog` skill pairs so well with the `commit` command. The command enforces Conventional Commits going in, and the skill relies on that structure coming out. The two halves of the plugin reinforce each other. ## The finished plugin Put together, the whole plugin is three files. The manifest names it, the command gives you an on-demand action, and the skill gives Claude an automatic one: ```text git-flow/ ├── .claude-plugin/ │ └── plugin.json # name, version, metadata ├── commands/ │ └── commit.md # /git-flow:commit, you invoke it └── skills/ └── changelog/ └── SKILL.md # changelog, Claude invokes it ``` There is no build step and no compilation. A plugin is plain text that Claude Code reads, which makes it easy to review before you install it and easy to keep in version control. ## Testing it locally Before you hand a plugin to anyone, validate it. The CLI checks the manifest, the command and skill frontmatter, and any hook files for syntax and schema problems: ```bash claude plugin validate ./git-flow claude plugin validate ./git-flow --strict ``` The `--strict` flag promotes warnings (like an unrecognized manifest field) to errors, which is what you want in CI. To actually try it, load the plugin into a single session straight from disk, no marketplace required: ```bash claude --plugin-dir ./git-flow ``` Now `/git-flow:commit` is live, and asking Claude to draft release notes should pull in the `changelog` skill. As you edit the files, run `/reload-plugins` inside the session to pick up your changes without restarting. This local loop is where you should spend most of your time: tighten the command's instructions, sharpen the skill's description until it triggers on the phrases you expect, and only then think about distribution. ## Publishing through a marketplace A plugin is a folder; a marketplace is a catalog that tells Claude Code where a set of plugins lives. It is a single file, `.claude-plugin/marketplace.json`, and it needs three things: a `name`, an `owner`, and a `plugins` array. Each entry in that array needs a `name` and a `source`. The simplest setup is one repository that holds both the marketplace file and the plugin, so the source is just a relative path: ```json { "name": "frank-claude-tools", "owner": { "name": "Frank Perez", "email": "frank@fjp.io" }, "plugins": [ { "name": "git-flow", "source": "./git-flow", "description": "Conventional Commits helper for Claude Code." } ] } ``` That gives a repository layout like this: ```text frank-claude-tools/ ├── .claude-plugin/ │ └── marketplace.json └── git-flow/ ├── .claude-plugin/ │ └── plugin.json ├── commands/ │ └── commit.md └── skills/ └── changelog/ └── SKILL.md ``` A relative path is not the only option. When the plugin lives in its own repository, point at it with a `github` source instead, optionally pinning a branch or tag with `ref` and an exact commit with `sha`: ```json { "name": "git-flow", "source": { "source": "github", "repo": "frankperez87/git-flow", "ref": "v0.1.0" } } ``` Once the marketplace is pushed to GitHub, anyone adds it once and then installs the plugins they want: ```bash /plugin marketplace add frankperez87/frank-claude-tools /plugin install git-flow@frank-claude-tools ``` The `git-flow@frank-claude-tools` form is why marketplace and plugin names both have to be unique: together they address exactly one plugin. By default the install is personal to you. To make a plugin part of a project so the whole team gets it on checkout, install it into the project scope, which records it in the repo's `.claude/settings.json`: ```bash /plugin install --scope project git-flow@frank-claude-tools ``` ## Versioning and releases How updates reach your users comes down to one decision: whether you set a `version` in `plugin.json`. There are two strategies, and they suit different situations. Set an explicit `version` and you have pinned a release. Users get the new code only when you bump that number, so pushing a quick fix to your repository does nothing until you also raise the version. That is the right default for anything other people depend on, paired with normal semantic versioning: bump the patch for a fix, the minor for a new command or skill, the major for a breaking change. If a version is set in both the manifest and the marketplace entry, the manifest wins. Omit `version` entirely and Claude Code falls back to the git commit SHA, which means every commit is treated as a new version. That is ideal while you are iterating on an internal tool and want teammates to always run the latest `main`, with no release ceremony at all. Either way, shipping an update is just pushing to the repository. Users pull the newest catalog with `/plugin marketplace update`, and a pinned plugin then upgrades to whatever version you have published. Cutting a release of `git-flow`, then, is three steps: raise the version in `plugin.json`, ask Claude to run the `changelog` skill so the release notes write themselves from your Conventional Commits, and push. The plugin you built ends up maintaining itself. ::note{label="Going further"} This only scratches the surface. Plugins can also bundle subagents, lifecycle hooks, and MCP servers, and marketplaces support git, npm, and monorepo sources. The full reference lives at [code.claude.com/docs/en/plugins](https://code.claude.com/docs/en/plugins){rel=""nofollow""}. Now go bottle up one of your own rituals. :: # Improve Your Git Workflow with Git Flow Git Flow is a development workflow for keeping branching consistent across a team. It was the workflow everyone copied for years, and it is still a great fit for certain kinds of projects. In this article I will walk through the branches it uses, show a hands-on example of features, releases and hotfixes, and give you an honest read on when to reach for it. ## Is Git Flow still the right choice? Let's get the reality check out of the way first, because it changes how you should read the rest of this post. Git Flow comes from Vincent Driessen's 2010 article "A successful Git branching model." In 2020 he added a note to the top of that same article saying that if your team practices continuous delivery, you should probably adopt something simpler like GitHub Flow, and that Git Flow still fits teams building explicitly versioned software or supporting multiple versions in the wild. Atlassian now describes Git Flow as a legacy workflow, one that has lost ground to trunk-based development for modern CI/CD. None of that makes it wrong. It just means the extra branches are overhead you only want to pay for when versioned releases actually earn it. Think desktop apps, libraries, mobile releases, SDKs, anything where multiple versions live in the world at once and need separate maintenance. If you are shipping a web app that deploys continuously, skip ahead to [GitHub Flow](https://franktheprogrammer.com/articles/github-flow-keep-your-main-branch-deployable), or read my [Git Flow vs GitHub Flow comparison](https://franktheprogrammer.com/articles/git-flow-vs-github-flow-choosing-a-branching-strategy) for the full head to head. Still here? Then you probably ship versions, and Git Flow's structure is going to serve you well. Let's get into it. ## The branches Git Flow uses two long-lived branches plus three kinds of supporting branches. - **`main`** is your currently released code, what you would find in production. (In the original 2010 model this branch was called `master`; modern repos default to `main`, and that is what I will use throughout.) - **`develop`** is the integration branch where finished work collects between releases. On top of those two, you create short-lived supporting branches as you need them. - **`feature/*`** branches come off `develop` for each new piece of work. - **`release/*`** branches freeze `develop` so you can harden a version before shipping it. - **`hotfix/*`** branches come off `main` to patch production in a hurry. The golden rule: you never commit directly to `main` or `develop`. Everything arrives through a supporting branch. ## Setting up the project The best way to learn this is by doing, so let's set up a fresh repository. Inside your project directory, create the first commit on `main` and push it up. ```bash echo "# Git Flow Example" >> README.md git init git add README.md git commit -m "Initial commit" git remote add origin git@github.com:frankperez87/gitflow-example.git git push -u origin main ``` That initial commit is the one exception to the never-touch-main rule; we are just bootstrapping the repo. From here on, `main` only changes through releases and hotfixes. Next, create the `develop` branch and track it remotely. This is where features will land. ```bash git branch develop git push -u origin develop ``` ## Feature branches Every new piece of work is a feature, unless it is an urgent production fix (that is a hotfix, which we will get to). Let's add a contributing guide. We branch off `develop` and prefix the branch with `feature/`. ```bash git checkout -b feature/contributing develop ``` Now make the change. Keep it small and real. ```bash echo "# Contributing" >> CONTRIBUTING.md echo "Open a pull request against develop and keep changes focused." >> CONTRIBUTING.md git add CONTRIBUTING.md git commit -m "Add contributing guide" ``` > You can also name the branch `feature-contributing` with a dash instead of a slash. Both are fine; just pick one convention and stick to it. With the feature done, merge it back into `develop` and delete the branch, since it has served its purpose. ```bash git checkout develop git merge feature/contributing git branch -d feature/contributing ``` ## Release branches When `develop` has enough finished work to ship, you cut a release branch. Release branches are prefixed with `release/` followed by the version number. This freezes a snapshot of `develop` so you can do final hardening (version bumps, last-minute fixes, changelog) without blocking new feature work. ```bash git checkout -b release/0.1.0 develop echo "Version 0.1.0" >> CHANGELOG.md git add CHANGELOG.md git commit -m "Prepare 0.1.0 release" ``` When the release is ready, merge it into `main`, then tag that commit with the version. ```bash git checkout main git merge release/0.1.0 git tag -a v0.1.0 -m "Release 0.1.0" git push origin main v0.1.0 ``` Here is the step people forget: you also merge the release back into `develop`. Any fixes or version bumps you made on the release branch happened off `develop`'s line, so merging them back keeps `develop` from falling behind production. ```bash git checkout develop git merge release/0.1.0 git push git branch -d release/0.1.0 ``` ## Hotfix branches Now suppose a user finds a bug in production that cannot wait for the next release. A hotfix branches straight off `main` (not `develop`), because you want to patch exactly what is live. ```bash git checkout -b hotfix/login-typo main ``` Make the smallest change that fixes the problem and commit it. ```bash git commit -am "Fix typo on login button" ``` Just like a release, a hotfix merges into **both** `main` and `develop` so the fix is not lost, and the new version gets tagged on `main`. A bug fix is a backward-compatible change, so it bumps the PATCH number: `0.1.0` becomes `0.1.1`. ```bash git checkout main git merge hotfix/login-typo git tag -a v0.1.1 -m "Hotfix: login button typo" git push origin main v0.1.1 git checkout develop git merge hotfix/login-typo git push git branch -d hotfix/login-typo ``` ## A quick word on versioning Those tags are not arbitrary. Git Flow pairs naturally with [semantic versioning](https://semver.org/){rel=""nofollow""}, where a version reads `MAJOR.MINOR.PATCH`: - **MAJOR** for a breaking change. - **MINOR** for new, backward-compatible functionality (your next feature release would be `v0.2.0`). - **PATCH** for a backward-compatible bug fix (the hotfix above, `v0.1.1`). Tag every release on `main` and your team, along with your users, can always check out exactly what shipped and when. ## The git flow CLI You do not have to type all of those merges and tags by hand. The `git flow` extension wraps the whole model into a handful of commands. ```bash git flow init # set up main, develop and the branch prefixes git flow feature start contributing # branch off develop git flow feature finish contributing # merge back into develop, delete the branch git flow release start 0.1.0 # branch off develop git flow release finish 0.1.0 # merge to main and develop, tag main git flow hotfix start 0.1.1 # branch off main git flow hotfix finish 0.1.1 # merge to main and develop, tag main ``` It is doing exactly the merges and tags you just learned, which is why it is worth understanding the manual flow first. One heads-up for 2026: the original `nvie/gitflow` extension was archived in October 2025 (it now points to a successor called git-flow-next), and the long-popular `gitflow-avh` fork was archived back in 2023. The model is the same either way, so I lean on the plain `git` commands above and treat the CLI as optional sugar. ## Wrapping up That is Git Flow end to end: two long-lived branches, supporting branches for features, releases and hotfixes, and a tag on `main` for every version. The structure shines when you ship versioned software on a schedule and have to maintain more than one release at a time. If that does not sound like your project, do not force it. The real win is that your whole team agrees on how branches are named, when things merge, and how releases get tagged. For a continuously deployed web app, that agreement usually looks like [GitHub Flow](https://franktheprogrammer.com/articles/github-flow-keep-your-main-branch-deployable) instead. Either way, pick one, write it down, and get everyone using it. # Interpolation in Stylus ::note{label="Stylus series"} Part of my [Learning Stylus](https://franktheprogrammer.com/articles/learning-stylus-a-css-pre-processor) series. Stylus still works in 2026, but its ecosystem has largely moved on to Sass, PostCSS, and native CSS; the series intro has the full context. :: You can also use interpolation to improve your functions for reuse, as well as your other code within your stylesheet. The way it works is that you can wrap your expression within {}, which will then be outputted as the identifier. ## Interpolating Properties For instance, looking back at our border-radius function, we can take it another step forward. Instead of calling border-radius directly, we can make a helper method that works for other styles as well. **Stylus Code** ```stylus vendor(prop, args) -webkit-{ prop }: args -moz-{ prop }: args { prop }: args .box width: 100px height: 100px background-color: blue vendor(border-radius, 10px) ``` Notice that now we can simply call our vendor function along with the style, followed by the arguments for any style. I personally prefer creating functions for specific styles. So let's look at another possibility. **Stylus Code** ```stylus vendor(prop, args) -webkit-{ prop }: args -moz-{ prop }: args { prop }: args border-radius() vendor(border-radius, arguments) .box width: 100px height: 100px background-color: blue border-radius: 15px ``` Combining both options we're able to come up with a far more readable approach, while keeping the repetitive code maintainable from a single location. ## Selector Interpolation You can also use interpolation within your selectors. Let's take an example where we loop through 10 divs and change the background-color of each alternating row. **Stylus Code** ```stylus for row in 1..10 .product-row:nth-child({row}) background-color: (row % 2 == 0) ? green : blue width: 100% height: 20px display: block ``` The above example loops through the number 1-10, while checking if each number is divisible by 2. When it is divisible by 2, then the background color is green, when it's not the background color is blue. There is a much better way to handle alternating colors, the above is just a simple example of showing you how you can easily use interpolation within your selectors. Here's the better example of accomplishing the above for you to reference. **Stylus Code** ```stylus .product-row width: 100% height: 20px display: block .product-row:nth-child(even) background-color: green .product-row:nth-child(odd) background-color: blue ``` You can also assign a list of selectors to a variable if needed. **Stylus Code** ```stylus $item = '.product, .item' {$item} background-color: green ``` **Stylus Output** ```css .product, .item { background-color: #008000; } ``` # Introduction to ECMAScript 6 The latest in ECMAScript 6 introduces new features to JavaScript which makes it so much more fun to use, while solving problems that have been around for years. The intent of this article is to provide you with resources you can use to start learning ES6 today. ::note{label="From 2026"} ES6 (released in 2015) is now simply "JavaScript" and is fully supported in every modern browser and Node.js, so its features are baseline rather than cutting-edge. The language has continued to evolve with yearly releases (ES2016 onward), but everything covered here is part of the standard toolkit today. :: ## What is ECMAScript 6? ES6 can be looked at as JavaScript's engine, where browsers and Node.js took it and extended on top of it to add specific objects and features related to each. ECMAScript itself is the standard that drives the current and future versions of JavaScript. As of this writing, the latest version, ES6, was released in 2015 which is why it is also known as ECMAScript 2015. It is more commonly referenced as ECMAScript 6 and ES6 though. Personally, lots of the new features have made JavaScript a lot easier to work with, simplifying the syntax and introducing an object-oriented way of building your JavaScript. ## Awesome New Features Some of the highlights introduced with ES6 include: - Block scoped variable types such as const and let. - Arrow functions. - Template strings. - Array and Object Destructuring. - Class definition and inheritance. - Enhanced regular expressions. - And much much more... ## Resources I have found the following resources amazing and helpful for getting caught up with ES6 and learning all of the new neat things you can do. - Feature Overview & Comparison: [es6-features.org](http://es6-features.org/){rel=""nofollow""} - Video Series by Jeffrey Way: [laracasts.com](https://laracasts.com/series/es6-cliffsnotes){rel=""nofollow""} - Video Series by Wes Bos: [es6.io](http://es6.io/){rel=""nofollow""} - Understanding ECMAScript 6 eBook: [leanpub.com](https://leanpub.com/understandinges6){rel=""nofollow""} # JavaScript's map, filter, and reduce methods JavaScript provides some amazing functions that can be called against your arrays to help filter them, manipulate them, or even reduce them down to a single value or grouped values. This article is intended to help you understand how you can use these 3 methods to simplify your existing applications. ## .map() First let's discuss the `.map()` method. You can use this function to manipulate data within an existing array. For instance, let's assume that you have an array of people. ```javascript const people = [ { name: 'Frank', gender: 'Male' }, { name: 'Sarah', gender: 'Female' }, { name: 'Michael', gender: 'Male' }, { name: 'Jessy', gender: 'Female' }, { name: 'David', gender: 'Male' }, { name: 'Jabari', gender: 'Male' }, ]; ``` Given the above array, let's say we want to extract an array of names. You could easily do this using the `.map()` method as follows: ```javascript const names = people.map(person => person.name); ``` This will return a new array of everyone's name. ```javascript Array (6) 0 "Frank" 1 "Sarah" 2 "Michael" 3 "Jessy" 4 "David" 5 "Jabari" ``` ## .filter() Another helpful method is `.filter()`. You can use this function when you're interested in getting a subset of your data based on a particular condition. As an example, we'll filter our list of people but only return the males. ```javascript const people = [ { name: 'Frank', gender: 'Male' }, { name: 'Sarah', gender: 'Female' }, { name: 'Michael', gender: 'Male' }, { name: 'Jessy', gender: 'Female' }, { name: 'David', gender: 'Male' }, { name: 'Jabari', gender: 'Male' }, ]; const men = people.filter(person => person.gender === 'Male'); ``` > As long as the condition you pass in returns true, it will include the given record into your new array. The output of the above example would be as follows. ```javascript [ {name: "Frank", gender: "Male"}, {name: "Michael", gender: "Male"}, {name: "David", gender: "Male"}, {name: "Jabari", gender: "Male"} ] ``` ## .reduce() Another super handy method is `.reduce()`. You can either reduce an entire array into a single value or even into groups as well. First let's look at an example where we get the count of how many men we have in the array. ```javascript const count = people.reduce((carry, person) => { if(person.gender === 'Male') { carry++; } return carry; }, 0) ``` The above example returns **4** as expected. We could add filter to the mix to make the code more readable. For instance the following will produce the exact same result. ```javascript const count = people .filter(person => person.gender === 'Male') .reduce((carry, person) => carry += 1, 0); ``` Let's look at another example. Maybe instead of returning a count, we'd prefer to return a new Object of our men and women separated into their own groups. We could easily accomplish that by doing the following. ```javascript const groups = people.reduce((accumulator, person) => { const group = person.gender === 'Male' ? 'men' : 'women'; accumulator[group].push(person); return accumulator; }, { men: [], women: [] }); ``` The above example would output the following: ```javascript { men: [ {name: "Frank", gender: "Male"}, {name: "Michael", gender: "Male"}, {name: "David", gender: "Male"}, {name: "Jabari", gender: "Male"} ], women: [ {name: "Sarah", gender: "Female"}, {name: "Jessy", gender: "Female"} ] } ``` # Learning Stylus: A CSS Pre-Processor How the content is displayed for this mini-series will be a little different to how you may see other articles on my site. Really this article is more geared as notes for me as I go through the documentation for Stylus, and learn the ins and outs of this beautiful language. Feel free to reference the full [Stylus documentation](https://stylus-lang.com/){rel=""nofollow""}. ::note{label="From 2026"} I'm keeping this series as I wrote it back in 2016. Stylus still works (its most recent release, 0.63.0, shipped in 2024 under Automattic's stewardship), but development has gone quiet and most projects have moved on to Sass, PostCSS, or plain CSS, which has since picked up native nesting and custom properties. The ideas below still translate cleanly to whichever of those you land on. :: One of the first cool things you'll see is that the braces, colons, and semi-colons are all optional. This allows you to come up with your own style that works for you. The language is indentation-based or pythonic, so make sure to indent your code to stay away from any hidden surprises. If you're interested in learning how to set up your project to work with Stylus, you can check out my article [here](https://franktheprogrammer.com/articles/configuring-stylus-css-pre-processor-with-gulp-and-sourcemaps). Like anything, to learn we need to dive in and start. - [Selectors](https://franktheprogrammer.com/articles/using-selectors-in-stylus-css-pre-processor) - [Variables](https://franktheprogrammer.com/articles/setting-variables-in-stylus-css-pre-processor) - [Functions and Mixins](https://franktheprogrammer.com/articles/using-functions-and-mixins-with-stylus-css-pre-processor) - [Interpolation](https://franktheprogrammer.com/articles/interpolation-in-stylus-css-pre-processor) - [Using JSON for Configuration Files](https://franktheprogrammer.com/articles/creating-configuration-files-in-stylus-css-pre-processor) # llms.txt, and who actually reads it `llms.txt` is a single Markdown file you drop at the root of your site, `/llms.txt`, that hands a language model a curated map of your content. It takes an afternoon to add and almost nothing can go wrong. What most write-ups skip is that in 2026 barely anything reads it. The one payoff that holds up is the one it was designed for: feeding documentation to coding agents. This blog ships one, so I want to walk through what it is, how to add it, and where it helps versus where it is wishful thinking. ## What llms.txt is The idea came from Jeremy Howard of Answer.AI (also behind fast.ai), published on [llmstxt.org](https://llmstxt.org/){rel=""nofollow""} on 3 September 2024. It targets two concrete problems. First, context windows are too small to swallow a whole website, so a model needs a shortlist of what matters. Second, HTML pages in the wild are wrapped in navigation, ads, cookie banners, and JavaScript, and turning that soup back into clean text is lossy and slow. A hand-written `llms.txt` sidesteps both: it is already plain text, and you decide what goes in it. Two things trip people up. It is a *proposal*, not a ratified standard, and the difference matters. `robots.txt` has a formal spec (RFC 9309) and `sitemap.xml` has multi-party backing; `llms.txt` is one party's suggestion with a GitHub repo and a Discord. Nothing wrong with that, but it means no provider is obligated to honor it. And it was pitched for *inference*, not training and not SEO. The intended moment is when someone is actively working and pulls your docs into a model right then, for example loading a library's reference into a coding session. Most of the confusion around the file comes from the SEO world repurposing it as a ranking lever it was never meant to be. ## The format, by example The spec is deliberately small. One required H1 with the site name, an optional blockquote summary, optional prose, then any number of `##` sections that are just Markdown link lists. Each link can carry a short note after a colon. A special `## Optional` section marks links a model can skip when it needs a shorter context. ```markdown # Acme Docs > Acme is a payments API for issuing virtual cards and moving money between accounts. Use the quickstart first; the API reference assumes you already have an API key. ## Docs - [Quickstart](https://acme.dev/docs/quickstart.md): install, authenticate, first request - [Cards API](https://acme.dev/docs/cards.md): create, freeze, and top up virtual cards - [Webhooks](https://acme.dev/docs/webhooks.md): event types and signature verification ## Optional - [Changelog](https://acme.dev/changelog.md) - [Brand assets](https://acme.dev/brand.md) ``` That is the whole grammar. The descriptions carry the weight here, so write them to disambiguate ("create, freeze, and top up virtual cards") rather than to sell ("our world-class card API"). The model is trying to find the page that answers a question, so tell it which one that is. One companion convention goes with it. Pages meant for models should offer a clean Markdown twin at the same URL with `.md` appended, so `cards.html` becomes `cards.html.md`, which is why the links above use `.md`. Two file names come up, and they do different jobs: - **`llms.txt`** is the curated *index*: title, summary, and links with one-line notes. Keep it small, a couple of kilobytes, ten to thirty links. - **`llms-full.txt`** is the *full dump*: every doc inlined into one file so a tool can ingest everything in a single fetch. These get enormous. Cloudflare's runs into the millions of tokens. Ship the index for almost anything. Add the full export when you have substantial docs and want single-fetch ingestion. ## Who actually reads it This is the part to be straight about, because it should change how much effort you spend. Coding agents do use it. This is the original use case and it still works. Cursor lets you add an `llms-full.txt` URL as a docs source, and Claude Code, Copilot, and MCP or RAG pipelines will fetch a docs `llms.txt` to orient before they write code against a library. If you maintain developer documentation, that audience exists and it is using the file today. AI search, on the other hand, mostly ignores it. In June 2026 Ahrefs published a study of 137,210 domains and found that of roughly 38,000 with a valid `llms.txt`, [97% received zero requests for it in May 2026](https://ahrefs.com/blog/llmstxt-study/){rel=""nofollow""}. Not "little traffic", none. And the sliver that did see traffic tells the same story from the other side: the [named AI tools fetching these files were led by GPTBot and Claude-Code](https://www.searchenginejournal.com/97-of-llms-txt-files-got-no-requests-ahrefs-data-shows/579478/){rel=""nofollow""}, which are coding and crawling agents, not the AI Overviews or chat answers people hope to rank in. Google has been blunt about it. Gary Illyes said at a Search Central Live event in mid-2025 that Google does not support `llms.txt` and is not planning to, and John Mueller compared it to the old keywords meta tag, a self-declared signal that got abandoned because it was trivial to game. Google's own AI-optimization guidance, updated in June 2026, [says plainly that Search does not use these files](https://searchengineland.com/google-says-normal-seo-works-for-ranking-in-ai-overviews-and-llms-txt-wont-be-used-459422){rel=""nofollow""} and that keeping one will neither help nor hurt your ranking. The distinction that clears up most of the noise: publishing is common, consumption is rare. Thousands of docs sites have the file, largely because platforms like Mintlify auto-generated it for everyone at once. That number says nothing about whether AI systems read it. For coding agents, yes. For getting cited in an AI answer, the evidence says no. ## Adding one to your site For most stacks you do not write this by hand. Documentation platforms like Mintlify, GitBook, and Fern auto-generate `llms.txt`, `llms-full.txt`, and clean Markdown versions of every page for hosted docs, and there are community plugins for the popular site generators: `vitepress-plugin-llms` for VitePress, `starlight-llms-txt` for Astro Starlight, plus options for MkDocs and Docusaurus, and a generator built into the Yoast plugin for WordPress. This site publishes both files that way, built from its content, and you can read them live at [/llms.txt](https://franktheprogrammer.com/llms.txt) and [/llms-full.txt](https://franktheprogrammer.com/llms-full.txt). For anything that changes often, generating the file from your content beats maintaining it by hand, because the usual failure mode is the file drifting out of sync with reality. If you would rather hand-roll it, or your stack has no plugin, it is just a text file: 1. Write the Markdown by hand using the format above. Curate; do not paste your sitemap. 2. Save it as `llms.txt` and serve it from the web root so it resolves at `https://yoursite.com/llms.txt`. 3. Confirm it returns HTTP 200 and comes back as plain text, not an HTML error page. 4. Check `robots.txt` is not blocking the path or the AI user-agents you actually want reading it. ::note{label="Heads up"} Everything in `llms.txt` is public and advisory. It is a broadcast, not a fence. Never list staging URLs, links with secrets in the query string, or anything behind auth, and do not treat it as access control. Blocking crawlers is `robots.txt`'s job. :: ## Doing it well The good version of this file is short and current. Curate ruthlessly. Ten to thirty high-value links beat a flattened sitemap. If you need more than that, `llms-full.txt` is where the bulk goes. Keep the summary factual. Read it back and ask whether it sounds like the opening line of a Wikipedia article or the top of a landing page. You want the former: what the thing *is*, no adjectives. Keep it in sync. A stale index full of dead links is worse than no index, because anything that does read it now distrusts it. Generate it from your content and it stays honest on its own. Do not keyword-stuff. No model is extracting keyword density from this file, and it reads as junk to any human who opens it. ## So should you bother? It depends on what you run. If you publish developer or API documentation, yes, and ship both files. Coding agents fetch them, structured docs make those agents more accurate and cheaper on tokens, and if you are on Mintlify, GitBook, or Nuxt Content it is nearly free. This is where the file earns the effort. If you run a marketing or content site, treat it as a cheap hedge and cap the effort at half a day. Curate your fifteen-to-thirty canonical pages, write plain descriptions, ship it, move on. Do not promise anyone AI-search gains from it, because the 2026 data does not back that up. Either way, the things that actually help a model understand your site are the unglamorous ones: clean semantic HTML, a correct `robots.txt` that lets in the AI user-agents you want, valid structured data, and content worth reading. `llms.txt` sits on top of those as a nice-to-have. Add it because it is cheap and points the right direction, not because you expect it to move a number. If you are already deep in the coding-agent workflow, it pairs well with the rest of that toolchain, and I have written more about [getting more out of Claude Code](https://franktheprogrammer.com/articles/getting-more-out-of-claude-code) and [building your own Claude Code plugin](https://franktheprogrammer.com/articles/how-to-build-a-claude-code-plugin) if that is where you are headed. # Loop Engineering in Claude Code: Designing the System That Prompts the Agent A tweet from Addy Osmani reframed how I think about working with Claude Code. He put it this way: "Loop engineering is replacing yourself as the person who prompts the agent. You design the system that does it instead. A loop here can be thought of a recursive goal where you define a purpose and the AI iterates until complete." He is not the only one saying it. Peter Steinberger put it more bluntly: "You shouldn't be prompting coding agents anymore. You should be designing loops that prompt your agents." And Boris Cherny, who heads Claude Code at Anthropic, said the quiet part out loud: "I don't prompt Claude anymore. I have loops running that prompt Claude and figuring out what to do. My job is to write loops." It is easy to wave this off as a cron job wearing a hat, and that reaction is not entirely wrong. But the framing stuck with me, because once you go looking, every piece you need to build one of these loops is already sitting inside Claude Code. This is a tour of those pieces, with the examples I actually reach for, and a straight answer to when a loop is worth it versus when you are just lighting tokens on fire. ## From prompting to designing the loop The familiar way to work with a coding agent is straightforward. You write a good prompt, give the agent enough context, read what comes back, and type the next thing. You are holding the tool the entire time, one turn after another. That works, and for a lot of tasks it is still the right way to work. Loop engineering is what happens when you stop being the thing in the loop. Instead of prompting each step, you build a small system that finds the work, hands it out, checks the result, writes down what happened, and decides what to do next, and you let *that* poke the agent. The "recursive goal" idea is the heart of it: you define a purpose once, and the agent iterates until that purpose is met. The part that surprised me is how little custom plumbing this takes. You might expect a loop to mean a pile of bash you own and babysit forever, but the pieces already ship inside the product itself. If you have read my notes on [prompting and the token economy](https://franktheprogrammer.com/articles/getting-more-out-of-claude-code), think of this as the layer above that: not how to phrase one turn well, but how to arrange many turns so you do not have to phrase each one at all. ## The building blocks Claude Code already ships Osmani's list is five pieces plus one place to remember things. Here they are, each mapped to the actual Claude Code mechanism. ### Automations: the heartbeat A loop is only a loop if something kicks it off again. In Claude Code that is `/loop` for in-session cadence, [hooks](https://franktheprogrammer.com/articles/claude-code-hooks-by-example) for firing shell commands at points in the agent lifecycle (`SessionStart`, `PreToolUse`, `PostToolUse`, `SubagentStop`, and more), and GitHub Actions when you want the thing to keep running after you close the laptop. You define an autonomous task, give it a trigger, and let the findings come to you instead of going around checking. ### Worktrees: so parallel does not become chaos The moment you run more than one agent, they start colliding on the same files. A git worktree is a separate working directory on its own branch sharing the same repo history, so one agent's edits cannot touch another's checkout. Claude Code leans on this directly: you can open a session in its own worktree, and you can give a subagent its own isolated checkout so each helper works in a fresh copy that cleans itself up afterward. The worktrees remove the mechanical collision, but your review bandwidth is still the ceiling on how many agents you can usefully run. ### Skills: stop re-explaining your project every session A [skill](https://franktheprogrammer.com/articles/how-to-build-a-claude-code-plugin) is a `SKILL.md` file in a folder under `.claude/skills/`, holding the project knowledge the agent would otherwise guess at every time: your conventions, your build steps, the "we don't do it like that because of one bad incident" rules. Without skills, a loop re-derives your whole project from zero on every cycle. With them, the knowledge compounds. The tight, boring description matters more than clever instructions, because that description is what tells the agent when the skill applies. ### Plugins and connectors: let the loop touch your real tools A loop that can only see the filesystem is a tiny loop. Connectors built on MCP (the Model Context Protocol) let the agent read your issue tracker, query a database, hit a staging API, or drop a message in Slack. Plugins are how you bundle skills and connectors together so a teammate installs your whole setup in one go. This is the difference between an agent that says "here is the fix" and a loop that opens the PR, links the ticket, and pings the channel once CI is green, by itself. ### Sub-agents: keep the maker away from the checker The single most useful structural move in a loop is splitting the agent that writes the code from the one that checks it. The model that wrote something is far too generous grading its own homework. In Claude Code you define a subagent as a Markdown file with frontmatter under `.claude/agents/`: ```markdown --- name: spec-reviewer description: Reviews a diff against the project spec and tests. Read-only. tools: Read, Glob, Grep model: sonnet --- You are a reviewer. Check the change against the existing tests and the conventions in the project's skills. Report what is wrong; do not fix it yourself. ``` The pattern I keep reaching for is one agent explores, one implements, and a different one verifies. That split is not a built-in feature with a fancy name, it is just how you wire the agents up. Subagents do cost more tokens, since each one runs its own model and tools, so spend them where a second opinion actually earns its keep. ### Memory: the agent forgets, the repo does not The sixth piece is the one that sounds too dumb to matter: a place to write down what is done and what is next, outside the conversation. A Markdown state file, a checklist in the repo, an issue tracker board, anything durable. The model forgets everything between runs, so the memory has to live on disk, not in the context window. This is the spine of any loop that runs more than once. ## The two primitives: `/loop` versus `/goal` Two in-session commands do most of the work, and the difference between them is the whole point. `/loop` re-runs a prompt or command on a **cadence**. Give it an interval and a prompt and it schedules the job: ```bash # fixed interval /loop 5m check if the deploy finished and tell me what happened # omit the interval and Claude self-paces between 1 minute and 1 hour, # waiting longer when nothing is happening /loop check whether CI passed and address any review comments # re-run a saved slash command each iteration /loop 20m /review-pr 1234 ``` A bare `/loop` runs a built-in maintenance prompt (continue unfinished work, tend the current PR, run cleanup passes). You can replace that with your own default by dropping a `.claude/loop.md` in the project or `~/.claude/loop.md` for all projects. Press `Esc` to stop a loop while it waits for the next iteration. A few guardrails apply: recurring tasks expire after 7 days, a session holds at most 50 scheduled tasks, and tasks are session-scoped, so they live in the current conversation and are restored only if you reopen it with `--resume`. `/goal` is the one closer to the recursive-goal idea. Instead of a clock, it runs until a condition you wrote is actually true: ```bash /goal all tests in test/auth pass and the linter is clean ``` After every turn, a separate, smaller model reads the condition and the conversation and returns a yes-or-no plus a short reason for the next turn. The agent that wrote the code is not the one deciding whether it is done. That is the maker/checker split applied to the stop condition itself, and it is why you can actually walk away. So: `/loop` for "keep checking on this," `/goal` for "keep working until this is true." When you need it to survive a closed laptop, push the whole thing to GitHub Actions instead. ## What one loop actually looks like This is the shape I keep coming back to, stitched together from the pieces above. A scheduled run fires on the repo every morning. Its prompt calls a triage skill that reads yesterday's CI failures, the open issues, and the recent commits, and writes the findings into a state file: ```markdown title=".claude/loop.md" Read the latest CI runs, open issues labeled `bug`, and commits since the last entry in `notes/triage.md`. For each problem worth fixing: 1. Open an isolated worktree and send a subagent to draft a minimal fix. 2. Send a second subagent to review that draft against the project's skills and the existing tests. It must not edit the code, only report. 3. If the review passes and CI is green, open a PR and link the issue. 4. If anything is unsure or risky, leave it in `notes/triage.md` for me. Append what you tried, what passed, and what is still open to `notes/triage.md` so tomorrow's run picks up where this one stopped. ``` Connectors open the PR and update the ticket. Anything the loop cannot handle safely lands in the notes file for me to look at. The state file is the part that makes it a loop and not a one-off, because tomorrow's run reads where today's stopped. Look at what you actually did there: you designed it once, and you prompted none of those individual steps. That is the pitch in one example, and it works the same way whether you run it through `/loop`, a `/goal`, or a GitHub Action. ## When to reach for a loop, and when not A loop earns its place when the work is repetitive, the trigger is clear, and there is a real signal for "done" that something other than the maker can check. Babysitting a deploy or a PR, triaging CI failures every morning, grinding a test suite to green, hunting a class of bug across a codebase: good fits, because each one has an automatic way to know it worked. A loop is the wrong tool when the goal is fuzzy and there is no verifier, when the action is irreversible and there is no human gate in front of it, or when the task is genuinely one-shot and a loop is just ceremony around a single prompt. If you cannot write the condition that means "done," you are not ready to write the loop yet. When you do build one, ramp it. Start in report-only mode, where the loop tells you what it would do. Move to assisted, where it drafts changes you approve. Only then let it run unattended, and even then keep a gate in front of anything you cannot undo. ## The subscription reality If you are on a Claude Code plan rather than paying per token, loops change your usage math, and not always gently. Osmani's own caveat is the right one to keep in mind: outcomes differ wildly between the token rich and the token poor. A few things to watch. Sub-agents multiply spend, because each one runs its own model and tool calls, so a three-agent explore/implement/verify loop costs roughly three times a single pass. A tight fixed interval burns tokens even when nothing is happening, so prefer a self-paced `/loop` (or `/goal`, which only runs when there is a turn to take) over `/loop 1m` on a quiet repo. Remember that `/loop` needs the session open and idle to fire and does not catch up on missed runs, so a babysat terminal is not the same thing as durable scheduling. Anything that genuinely needs to run while you sleep belongs in GitHub Actions, not a window you have to leave open. And treat the 7-day expiry as a feature: it bounds how long a forgotten loop can quietly run up your usage. When in doubt, start in report-only mode, where the loop reads a lot and writes almost nothing. For more on keeping spend sane, see [Getting More Out of Claude Code](https://franktheprogrammer.com/articles/getting-more-out-of-claude-code). ## Stay the engineer The loop changes the work; it does not delete you from it. Three problems actually get sharper as the loop gets better, not easier. Verification is still on you. A loop running unattended is also a loop making mistakes unattended, and "done" is a claim, not a proof. That separate verifier is what makes the claim mean something, which is the entire reason to bother with it. Your understanding rots if you let it. The faster a loop ships code you did not write, the wider the gap between what exists and what you actually grasp. A smooth loop just grows that gap faster unless you read what it made. And the comfortable posture is the dangerous one. When the loop runs itself, it is tempting to stop having an opinion and take whatever comes back. The same loop, in two different hands, produces opposite results: one person uses it to move faster on work they understand deeply, the other uses it to avoid understanding the work at all. The loop cannot tell the difference. You can. ::note{label="The short version"} Loop engineering is designing the system that prompts the agent instead of prompting it yourself. Claude Code already ships the pieces: `/loop` and `/goal` for the heartbeat, worktrees for isolation, skills for knowledge, MCP connectors for reach, sub-agents for a maker and a separate checker, and a state file for memory. Reach for it on repetitive work with a verifiable "done," start in report-only mode, watch your token spend, and build the loop like someone who intends to stay the engineer, not just the person who presses go. :: # Orchestrating Lambdas with Step Functions Across [part one](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) and [part two](https://franktheprogrammer.com/articles/give-your-lambda-an-http-front-door) we built a containerized thumbnailer and gave it an HTTP front door. One function behind one endpoint goes a long way. This finale is about the moment it stops being enough, when the work becomes several steps with retries, branching, and things running in parallel, and whether AWS Step Functions are the right answer. Sometimes they are not, and I will be honest about that before we build one. ## When do you actually need a state machine? You can get remarkably far without orchestration. A single Lambda, a Lambda calling another with the SDK, or API Gateway routing to a few functions covers most applications. Step Functions earn their place when you have a genuine multi-step **workflow** and you would rather not hand-write the glue: explicit steps, automatic retries with backoff on each one, parallel branches, conditional paths, waits, and a visual record of every execution. The tell is simple. If you catch yourself writing a Lambda whose entire job is to call three other Lambdas in order, catch each one's errors, retry the flaky ones, and stitch the results together, that orchestration code is exactly what a state machine replaces, and the state machine version is easier to see and to change. But if you have one function, or two calls in a row that rarely fail, you do not need this. Reaching for Step Functions there just adds a moving part. Our thumbnailer is a good candidate for one specific reason: we want several sizes generated **in parallel**, each with its own retry, and a tidy summary at the end. That is orchestration, so let us wire it up. ## Standard or Express? Step Functions come in two workflow types. **Standard** runs for up to a year, guarantees exactly-once execution, keeps a full visual history, and bills per state transition. **Express** runs for up to five minutes, is at-least-once, and bills per request plus duration, which makes it cheap at very high volume (think streaming or event ingestion). For an image pipeline that runs occasionally and where you want to inspect each run, Standard is the right call, and the execution history alone is worth it the first time something misbehaves. ## The pipeline The shape we want: take an input image, confirm it is valid, fan out to generate small, medium, and large versions at the same time, then summarize. Each size is a call to the thumbnailer Lambda from part one, and they run concurrently. One thing to design around up front: Step Functions cap the data passed between states at 256 KB. Shipping three full base64 PNGs through the workflow would blow past that fast, so a real pipeline writes the images to S3 inside the function and passes back keys or URLs, not the bytes. To keep the example readable we will just pass back each thumbnail's dimensions. ## The state machine, in JSONata Workflows are written in the Amazon States Language (ASL), which is JSON. Since late 2024 you can opt into **JSONata** as the query language, and it is a big simplification over the old JSONPath fields. You set it once at the top, then transform data with `{% ... %}` expressions and a reserved `$states` variable. Save this as `statemachine/pipeline.asl.json`: ```json { "Comment": "Generate thumbnails in parallel", "QueryLanguage": "JSONata", "StartAt": "CheckInput", "States": { "CheckInput": { "Type": "Choice", "Choices": [ { "Condition": "{% $exists($states.input.image_url) %}", "Next": "GenerateSizes" } ], "Default": "MissingImage" }, "GenerateSizes": { "Type": "Map", "Items": "{% [120, 480, 1024].{'width': $, 'image_url': $states.input.image_url} %}", "ItemProcessor": { "ProcessorConfig": { "Mode": "INLINE" }, "StartAt": "ResizeOne", "States": { "ResizeOne": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "Arguments": { "FunctionName": "${ThumbnailerArn}", "Payload": "{% $states.input %}" }, "Retry": [ { "ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"], "IntervalSeconds": 1, "MaxAttempts": 3, "BackoffRate": 2, "JitterStrategy": "FULL" } ], "Output": "{% {'width': $states.result.Payload.width, 'height': $states.result.Payload.height} %}", "End": true } } }, "Next": "Summarize" }, "Summarize": { "Type": "Pass", "Output": "{% {'generated': $count($states.input), 'sizes': $states.input} %}", "End": true }, "MissingImage": { "Type": "Fail", "Error": "MissingImage", "Cause": "Provide image_url in the input" } } } ``` There is a lot in there, so let's go through the pieces. ## Map, Choice, and the JSONata bits The `Choice` state branches on a condition. In JSONata mode, that is a single `Condition` expression that returns a boolean: `{% $exists($states.input.image_url) %}` checks the input actually has an image before we do any work, and falls through to `MissingImage` otherwise. `GenerateSizes` is a `Map` state, which runs its inner steps once per item, concurrently. The clever part is `Items`: the JSONata expression `[120, 480, 1024].{'width': $, 'image_url': $states.input.image_url}` turns the list of widths into a list of payloads, one per size, each carrying the original image URL. Each parallel iteration then runs `ResizeOne`. `ResizeOne` is the `Task` that calls Lambda. A few things to notice: - `"Resource": "arn:aws:states:::lambda:invoke"` is the optimized Lambda integration. The function's own return value comes back nested under `$states.result.Payload`, which is why the `Output` reaches in for `Payload.width`. - `Arguments` is what you send (the JSONPath world called this `Parameters`). `Output` is what the state passes on. Those two fields replace the five fiddly JSONPath fields, and there is no more `.$` suffix to remember. - `${ThumbnailerArn}` is **not** JSONata. It is a SAM substitution that gets replaced with the real function ARN at deploy time, which we set up next. JSONata runs at execution time inside `{% %}`; the `${...}` token is resolved earlier, when the template is deployed. They sit side by side happily. Finally, `Summarize` is a `Pass` state that just shapes the output: `$count($states.input)` counts the array the Map produced. ## Retries and catches Notice the `Retry` block on `ResizeOne`. Each parallel branch retries Lambda throttling and transient service errors on its own, backing off exponentially with jitter, before giving up. That per-step resilience is one of the strongest reasons to use a state machine instead of hand-rolled orchestration. For failures you want to handle rather than retry, add a `Catch` that routes to another state: ```json "Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "ReportFailure" } ] ``` `Retry` and `Catch` work together: the state retries while it can, and if it still fails, the catch sends execution down a recovery path of your choosing instead of failing the whole workflow. ## Deploy it with SAM We can extend the same `template.yaml` from part two. The state machine references the external ASL file, injects the function ARN through `DefinitionSubstitutions`, and is granted permission to invoke the function with a SAM policy template: ```yaml ThumbnailPipeline: Type: AWS::Serverless::StateMachine Properties: Type: STANDARD DefinitionUri: statemachine/pipeline.asl.json DefinitionSubstitutions: ThumbnailerArn: !GetAtt Thumbnailer.Arn Policies: - LambdaInvokePolicy: FunctionName: !Ref Thumbnailer ``` `DefinitionSubstitutions` is where `${ThumbnailerArn}` from the ASL gets its value, here the real ARN of the `Thumbnailer` function defined earlier in the template. `LambdaInvokePolicy` is a SAM shorthand that attaches the right IAM permission to the state machine's role so it can actually call the function. `sam build && sam deploy` ships it alongside the Lambda. ## Test it locally You can exercise a state machine without deploying the whole thing. AWS publishes **Step Functions Local** as a Docker image: ```bash docker run -p 8083:8083 amazon/aws-stepfunctions-local ``` You then point the CLI at it with `--endpoint http://localhost:8083` to create and start executions, and you can mock the Lambda calls so nothing touches AWS. For checking a single state's logic, the newer `TestState` API is even handier, since it runs one state with an input you provide: ```bash aws stepfunctions test-state \ --definition file://one-state.json \ --role-arn arn:aws:iam::111122223333:role/StepFunctionsRole \ --input '{"image_url": "https://example.com/cat.jpg"}' ``` And the individual functions are still just Lambdas, so `sam local invoke` from part two tests each one in isolation. Between unit-testing states, mocking the orchestration locally, and invoking the real functions, you can build a fair amount of confidence before deploying. That closes the series. We started with a single Python function in a Docker image, gave it a public HTTP endpoint and called it from Laravel, and finished by orchestrating it into a parallel pipeline with retries and branching. The throughline is that AWS gives you a ladder of options, direct invocation, function URLs, API Gateway, and Step Functions, and the skill is matching the rung to the job rather than always reaching for the top one. Start with the simplest thing that works, and climb only when the problem makes you. ::note{label="AWS Lambda series"} The final part of a hands-on series on running containerized Lambdas. Previous: [Give Your Lambda an HTTP Front Door](https://franktheprogrammer.com/articles/give-your-lambda-an-http-front-door) :: # Package a Python Lambda as a Docker Image AWS Lambda has a reputation for being all about little zip files, but it has supported Docker container images since 2020, and for a lot of real work they are the better fit. In this series we will build a small but genuinely useful Python function, a thumbnail generator, and take it from a Dockerfile all the way to something you can call over HTTP and even orchestrate into a workflow. This first part is the foundation: package the function as an image, pick an architecture, run it locally, push it to ECR, and invoke it directly. ## Container image or zip? When to reach for Docker By default a Lambda is a `.zip`: your code plus dependencies, uploaded directly or layered on. That path is wonderful for small, pure-Python functions. It supports Lambda layers, editing in the console, and SnapStart for faster cold starts. The catch is size: the deployment package is capped at 50 MB zipped on a direct upload and 250 MB unzipped including layers. Container images raise that ceiling to 10 GB and hand you the whole build. Reach for an image when: - Your dependencies are large (data or machine learning libraries) or need **native system libraries** (image and video tooling, headless browsers, compiled extensions). - You want your **own base image** and a reproducible `docker build`, ideally the same one your CI already produces. - Your team simply lives in Docker and would rather not learn the zip-and-layers dance. The tradeoffs are honest ones: container images do not support SnapStart (that is zip only), and you own patching the base image by rebuilding and redeploying. Our thumbnailer leans on Pillow, which ships native image codecs, so an image is the natural fit and a good excuse to learn the workflow. ## The function Here is the whole thing. It takes an image as base64 or a URL, resizes it while preserving aspect ratio, and returns the result as base64 so it travels happily inside JSON. Save it as `app.py`. ```python import base64 import io from urllib.request import urlopen from PIL import Image def handler(event, context): width = int(event.get("width", 200)) # Accept either a base64-encoded image or a URL to fetch. if "image_base64" in event: raw = base64.b64decode(event["image_base64"]) elif "image_url" in event: with urlopen(event["image_url"]) as response: raw = response.read() else: raise ValueError("Provide image_base64 or image_url") image = Image.open(io.BytesIO(raw)) image.thumbnail((width, width)) # resize in place, keeping aspect ratio buffer = io.BytesIO() image.save(buffer, format="PNG") return { "width": image.width, "height": image.height, "thumbnail_base64": base64.b64encode(buffer.getvalue()).decode(), } ``` A Lambda handler is just a function that takes `(event, context)`. The `event` is whatever invoked it sends; the `context` carries runtime metadata. Everything else here is ordinary Python, which is exactly the point. ## The Dockerfile AWS publishes base images that already speak the Lambda Runtime API, so the Dockerfile is short. Put `Pillow` in a `requirements.txt` next to `app.py`, then: ```dockerfile FROM public.ecr.aws/lambda/python:3.13 # Dependencies first, so Docker can cache this layer between builds. COPY requirements.txt ${LAMBDA_TASK_ROOT}/ RUN pip install --no-cache-dir -r requirements.txt # Then the function code. COPY app.py ${LAMBDA_TASK_ROOT}/ # "module.function" is the handler() in app.py. CMD ["app.handler"] ``` `${LAMBDA_TASK_ROOT}` is `/var/task`, where Lambda looks for your code. The base image already sets the `ENTRYPOINT` to its runtime bootstrap, so you only supply the handler name as the `CMD`. Do not override the entrypoint or add a `USER`; Lambda manages that for you. The `3.13` tag is a solid default (Python 3.13 on Amazon Linux 2023); `3.14` is the newest if you want the latest, and both run on either CPU architecture. ## arm64 or x86\_64? Lambda runs on two architectures: x86\_64 (the default) and arm64, which is AWS Graviton2. AWS quotes [up to 34% better price performance](https://aws.amazon.com/blogs/aws/aws-lambda-functions-powered-by-aws-graviton2-processor-run-your-functions-on-arm-and-get-up-to-34-better-price-performance/){rel=""nofollow""} on arm64 along with a lower per-millisecond price, so arm64 is the sensible default unless something stops you. With containers there is one rule to internalize: **an image is built for exactly one architecture**, and the function's architecture has to match it. Every native dependency has to have an arm64 build too. Pillow does, as do almost all popular packages now, but it is the thing to check before you switch. You select the architecture at build time: ```bash # Recommended default: arm64 (Graviton) docker buildx build --platform linux/arm64 -t thumbnailer:arm64 . # Or x86_64, if a dependency forces your hand docker buildx build --platform linux/amd64 -t thumbnailer:amd64 . ``` If you are on an Apple Silicon Mac, arm64 is also your native build, so it is both the faster thing to build locally and the cheaper thing to run. A happy alignment. ## Run it locally with the Runtime Interface Emulator This is the part people miss: the AWS base images bundle the **Runtime Interface Emulator** (RIE), a tiny local stand-in for the Lambda service. Start the container and it listens like the real thing: ```bash docker run --rm -p 9000:8080 thumbnailer:arm64 ``` Then, in another terminal, post an event to the emulator's invocation endpoint. That URL is fixed; the literal word `function` is part of it and the emulator ignores the function name: ```bash curl "http://localhost:9000/2015-03-31/functions/function/invocations" \ -d '{"image_url": "https://httpbin.org/image/jpeg", "width": 120}' ``` You get back the handler's return value as JSON: ```json { "width": 120, "height": 120, "thumbnail_base64": "iVBORw0KGgo..." } ``` To actually see the thumbnail, decode that field to a file: ```bash curl -s "http://localhost:9000/2015-03-31/functions/function/invocations" \ -d '{"image_url": "https://httpbin.org/image/jpeg", "width": 120}' \ | python3 -c "import sys, json, base64; open('thumb.png', 'wb').write(base64.b64decode(json.load(sys.stdin)['thumbnail_base64']))" ``` Open `thumb.png` and there is your resized image, produced by the exact bytes you are about to ship. (One gotcha: if you built an arm64 image but are running on an x86 host, add `--platform linux/arm64` to the `docker run` so it emulates the right CPU.) ## Push the image to Amazon ECR Lambda pulls container images from Amazon Elastic Container Registry, in the same Region as the function. Three steps: create a repository, log Docker in, then tag and push. ```bash # 1. Create the repository (once per repo) aws ecr create-repository --repository-name thumbnailer --region us-east-1 # 2. Authenticate Docker to your private registry aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com # 3. Tag the local image with the registry URI and push it docker tag thumbnailer:arm64 111122223333.dkr.ecr.us-east-1.amazonaws.com/thumbnailer:latest docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/thumbnailer:latest ``` Swap `111122223333` for your AWS account ID. The registry URI follows the pattern `.dkr.ecr..amazonaws.com/:`. ## Create the function and invoke it directly With the image in ECR, create the function from it. You need an execution role; a basic one that allows writing logs is enough to start. ```bash aws lambda create-function \ --function-name thumbnailer \ --package-type Image \ --code ImageUri=111122223333.dkr.ecr.us-east-1.amazonaws.com/thumbnailer:latest \ --role arn:aws:iam::111122223333:role/lambda-thumbnailer-role \ --architectures arm64 \ --memory-size 512 \ --timeout 30 ``` Now invoke it. No API Gateway, no HTTP, just the Lambda API directly, which is perfect for internal tools and scripts: ```bash aws lambda invoke \ --function-name thumbnailer \ --cli-binary-format raw-in-base64-out \ --payload '{"image_url": "https://httpbin.org/image/jpeg", "width": 120}' \ response.json ``` The `--cli-binary-format raw-in-base64-out` flag is required on AWS CLI v2 when you pass an inline JSON payload. The command prints `{"ExecutedVersion": "$LATEST", "StatusCode": 200}` and writes the function's output to `response.json`. One sharp edge to know about: `StatusCode` is 200 even when your code throws. A handler error shows up as a `FunctionError` field instead, and `--log-type Tail` returns the logs so you can see what happened. From application code it is the same idea. In Python with boto3: ```python import json import boto3 lambda_client = boto3.client("lambda") response = lambda_client.invoke( FunctionName="thumbnailer", InvocationType="RequestResponse", # synchronous; use "Event" to fire and forget Payload=json.dumps({"image_url": "https://httpbin.org/image/jpeg", "width": 120}), ) result = json.load(response["Payload"]) print(result["width"], result["height"]) ``` And because this is a PHP blog at heart, the AWS SDK for PHP does it just as cleanly. We will wire this into a real Laravel app in part two, but the call itself is a few lines: ```php use Aws\Lambda\LambdaClient; $lambda = new LambdaClient(['region' => 'us-east-1', 'version' => 'latest']); $result = $lambda->invoke([ 'FunctionName' => 'thumbnailer', 'Payload' => json_encode(['image_url' => 'https://example.com/cat.jpg', 'width' => 120]), ]); $thumbnail = json_decode($result->get('Payload')->getContents(), true); ``` After you change the code, rebuild the image, push it, and tell Lambda to pick up the new version with `aws lambda update-function-code --function-name thumbnailer --image-uri ...:latest --publish`. Lambda resolves a tag to a specific image digest at deploy time, so pushing a new `:latest` does not update the function on its own; the update command is what does it. You now have a containerized Python function tested locally with the very same emulator AWS runs, living in ECR, and callable directly. Invoking by hand or from a script is great for internal and event-driven use, but if you want a real URL that the world can hit, you need a front door. That is part two, where we compare Lambda function URLs against API Gateway and then call the result from a Laravel app. ::note{label="AWS Lambda series"} This is part one of a hands-on series on running containerized Lambdas. Next: [Give Your Lambda an HTTP Front Door](https://franktheprogrammer.com/articles/give-your-lambda-an-http-front-door) :: # Using PHP's array_map to format your arrays without loops So let's face it, loops are a bit boring. So how can we mix it up? Let's assume we have a case where we have a CSV file that we want to quickly parse. The CSV's first row contains the headers for each column of the CSV. How can we setup a new array with each key of being the name of the header field and the corresponding values assigned to them without using loops? I'll show you. First thing we'll want to understand is that PHP offers an alternative way to iterate through an array with the array\_map function. For this example we'll take a CSV of the U.S. state names and abbreviations. From this file we'll want to output the data organized into an array where each field header name relates to the correct field value. First let's get a CSV. I used this file [here](https://franktheprogrammer.com/data/states.csv). Next we'll want to write some code to parse out our file and extract the file's header for later use. ```php Array ( [State] => Alabama [Abbreviation] => AL ) [2] => Array ( [State] => Alaska [Abbreviation] => AK ) [3] => Array ( [State] => Arizona [Abbreviation] => AZ ) [4] => Array ( [State] => Arkansas [Abbreviation] => AR ) [5] => Array ( [State] => California [Abbreviation] => CA ) ``` See how simple that was? The code is now much simpler. Using this approach I believe also helps explain the intent behind your code more, making it easier for future developers to read your code base. # PHP's array_reduce is not only for outputting single values Sometimes we need to partition our data, which can be done in many ways. In this article I'll show you how to do this using PHP's array\_reduce function. First lets look at parameters that you can pass the function. - The first parameter should always be an array. - The second parameter is a call back function, which is where all the magic will happen. - The third, optional, parameter is where we set the initial value. First let's look at the basic usage by adding up an array of numbers. > You can also use `array_sum`, but for the purposes of this tutorial we'll use `array_reduce` ```php $numbers = [1, 3, 9, 12, 15, 18, 21]; $result = array_reduce($numbers, function($carry, $number) { return $carry + $number; }, 0); ``` Our result would be *79* as expected. Next we'll look at splitting up the numbers array into 2 groups. Small numbers being anything under 10, and large numbers being 10 and over. ```php $numbers = [1, 3, 9, 12, 15, 18, 21]; $result = [ 'small' => [], 'large' => [], ]; $result = array_reduce($numbers, function($carry, $number) { if($number >= 10) { array_push($carry['large'], $number); } if($number < 10) { array_push($carry['small'], $number); } return $carry; }, $result); ``` The output of $result would be the following as we expect: ```php Array ( [small] => Array ( [0] => 1 [1] => 3 [2] => 9 ) [large] => Array ( [0] => 12 [1] => 15 [2] => 18 [3] => 21 ) ) ``` Lastly, we'll take another example of taking a group of users and splitting them up by gender. ```php $users = [ ['name' => 'Frank', 'gender' => 'Male'], ['name' => 'Michael', 'gender' => 'Male'], ['name' => 'David', 'gender' => 'Male'], ['name' => 'Jabari', 'gender' => 'Male'], ['name' => 'Sarah', 'gender' => 'Female'], ['name' => 'Jessy', 'gender' => 'Female'], ['name' => 'Tracey', 'gender' => 'Female'], ]; $usersByGender = array_reduce( $users, function($groups, $user) { if($user['gender'] === 'Male') { array_push($groups['male'], $user); } if($user['gender'] === 'Female') { array_push($groups['female'], $user); } return $groups; }, [ 'male' => [], 'female' => [], ] ); ``` As expected our output would be the following: ```php Array ( [male] => Array ( [0] => Array ( [name] => Frank [gender] => Male ) [1] => Array ( [name] => Michael [gender] => Male ) [2] => Array ( [name] => David [gender] => Male ) [3] => Array ( [name] => Jabari [gender] => Male ) ) [female] => Array ( [0] => Array ( [name] => Sarah [gender] => Female ) [1] => Array ( [name] => Jessy [gender] => Female ) [2] => Array ( [name] => Tracey [gender] => Female ) ) ) ``` # FizzBuzz in PHP: A Fresh Approach A popular question asked during interviews is to apply the FizzBuzz logic to a range of numbers. This normally means the following: 1. If a number is divisible by 3, print Fizz. 2. If a number is divisible by 5, print Buzz. 3. If a number is divisible by 3 and 5, or 15, print FizzBuzz. Most examples out there demonstrate solving this problem using traditional loops. I want to try something different though. Let's solve this using the array\_map function. So let's assume the given range is from 1 to 100. Instead of writing this: *(using loops and a function)* ```php function translate($number) { if($number % 15 === 0) { return 'FizzBuzz'; } if($number % 3 === 0) { return 'Fizz'; } if($number % 5 === 0) { return 'Buzz'; } return $number; } $results = []; for($number = 1; $number <= 100; $number++) { $results[] = translate($number); } ``` We can actually solve it this way below ```php $results = array_map(function($number) { if($number % 15 === 0) return 'FizzBuzz'; if($number % 3 === 0) return 'Fizz'; if($number % 5 === 0) return 'Buzz'; return $number; }, range(1,100)); ``` Definitely cleaner to me, and much easier to consume. # PHP Group Multiple use Declarations As of PHP 7, you can now group your imported classes, functions, and constants from under the same namespace. Let's look at an example to show you how you can start using this today in your projects. Previously you'd have to write out your imports like so: ```php 1; echo 1.5 <=> 1.5; echo "a" <=> "a"; ``` Here are some examples of comparisons that would return -1. You'll see that the first value is less than the second value. ```php 2; echo 1.5 <=> 2.5; echo "a" <=> "b"; ``` Lastly, we have some examples where the first value is greater than the second value generating a positive 1 value. ```php 1; echo 2.5 <=> 1.5; echo "b" <=> "a"; ``` **Example Usage** Let's assume we have a list of products, and want to sort each product by price descending. First we'll create a Product class to hold our product information. ```php name = $name; $this->price = $price; } public function name() : string { return $this->name; } public function price() : float { return $this->price; } } ``` Next, let's create an array of new products. ```php price() <=> $product1->price(); }); ``` If we want to sort it instead by price ascending, then we can simply flip the order of how we're comparing $product1 and $product2. ```php price() <=> $product2->price(); }); ``` We can use this same logic to sort really by any parameter/attribute for each product. ::note{label="From 2026"} The spaceship operator has been baseline since PHP 7 and remains the idiomatic way to write `usort` comparators. It works across the usual type rules, so it also gives you a clean tiebreaker chain, for example `[$a->last, $a->first] <=> [$b->last, $b->first]`. :: # Python for PHP Developers If you write modern PHP, you already think in the right shapes: types, classes, enums, `match`, named arguments, immutability. Python has all of those too, it just spells them differently, with indentation instead of braces and `snake_case` instead of `camelCase`. This guide maps what you know onto Python so you can start reading and writing it quickly, without relearning how to program. If you followed my [AWS Lambda series](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image), those handlers were Python; this is the language behind them. ## Running code and managing packages In PHP you declare dependencies in `composer.json`, run `composer install`, get a `vendor/` folder and an autoloader, and run a script with `php script.php`. Python is the same idea with different nouns. ```php // composer.json lists dependencies, then: composer install require __DIR__ . '/vendor/autoload.php'; echo "Hello from PHP\n"; ``` ```python # requirements.txt lists dependencies. Work inside a virtual environment: print("Hello from Python") ``` The one new habit is the **virtual environment**. Instead of a per-project `vendor/`, you create and "activate" an isolated environment so each project has its own packages: ```bash python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install requests python hello.py ``` Think of `venv` as a `vendor/` directory you switch on, `pip` as Composer, and `requirements.txt` as the dependency list. The other immediate shift: no `$` on variables, no semicolons, and no curly braces. Indentation *is* the block syntax, so a misaligned line is a real error, not a style nit. ## Syntax basics Less ceremony, same ideas. The thing you will miss most at first is string interpolation, and Python's f-strings fill the gap nicely. ```php $name = "Ada"; $count = 3; echo "Hello, {$name}! You have {$count} messages.\n"; // a comment ``` ```python name = "Ada" count = 3 print(f"Hello, {name}! You have {count} messages.") # a comment ``` An f-string is any string prefixed with `f`, and `{}` interpolates an expression, much like `"{$var}"` in PHP. `print()` adds a newline for you, so you rarely type `\n`. ## Types and type hints Everything you learned about types in the PHP 8 era carries over, and the syntax is close enough to read on sight. ```php function greet(string $name, ?int $times = 1): string { return str_repeat("hi $name ", $times); } ``` ```python def greet(name: str, times: int | None = 1) -> str: return f"hi {name} " * (times or 1) ``` `?int` becomes `int | None`, the parameter type goes after a colon, and the return type follows `->`. The one crucial difference: **Python does not enforce these hints at runtime.** They document intent and power tools like `mypy` and your editor, but Python will happily run with the wrong type. PHP actually checks them. So in Python, hints are a strong convention rather than a guarantee. ## Arrays become lists and dicts This is the biggest mental adjustment. PHP's single `array` type does two jobs: ordered lists and keyed maps. Python splits them into two types, a `list` and a `dict`. ```php $nums = [1, 2, 3]; $user = ["name" => "Ada", "role" => "admin"]; foreach ($nums as $n) { echo $n; } foreach ($user as $key => $value) { echo "$key=$value"; } ``` ```python nums = [1, 2, 3] # a list user = {"name": "Ada", "role": "admin"} # a dict for n in nums: print(n) for key, value in user.items(): print(f"{key}={value}") ``` You iterate a dict's pairs with `.items()` rather than `as $key => $value`. And the array functions you use to avoid loops have direct counterparts, though the idiomatic Python is usually a comprehension: ```php $doubled = array_map(fn ($n) => $n * 2, $nums); $evens = array_filter($nums, fn ($n) => $n % 2 === 0); $total = array_reduce($nums, fn ($carry, $n) => $carry + $n, 0); ``` ```python doubled = [n * 2 for n in nums] # list comprehension evens = [n for n in nums if n % 2 == 0] total = sum(nums) # functools.reduce exists for the general case ``` A list comprehension reads as "this expression, for each item, optionally where a condition holds." Once it clicks, it replaces most of your `array_map`/`array_filter` reflexes. (For more on that PHP style, see [array\_map without loops](https://franktheprogrammer.com/articles/php-array-map-to-format-your-arrays-without-loops).) ## Functions Defaults, named arguments, and variadics all have clean equivalents. PHP 8's named arguments in particular map almost one to one. ```php function box(int $w, int $h, string $color = "black"): array { return compact("w", "h", "color"); } $b = box(w: 4, h: 3, color: "red"); // named arguments (PHP 8.0) function total(...$nums): int { return array_sum($nums); } $add = fn ($a, $b) => $a + $b; // arrow function ``` ```python def box(w: int, h: int, color: str = "black") -> dict: return {"w": w, "h": h, "color": color} b = box(w=4, h=3, color="red") # keyword arguments def total(*nums): # *args collects extra positionals return sum(nums) def add(a, b): # prefer def; lambda exists for one-liners return a + b ``` PHP's `name: value` named arguments become `name=value` keyword arguments. Variadic `...$nums` becomes `*nums`, and Python adds `**kwargs` for collecting keyword arguments into a dict. Arrow functions (`fn () =>`) have a `lambda` equivalent, but a named `def` is usually clearer and is what you will see most. ## Classes and objects Python is object-oriented to its core, so this will feel familiar, with two differences worth internalizing: `self` is explicit, and "private" is a convention rather than a rule. ```php final class Money { public function __construct( public readonly int $amount, private string $currency = "USD", ) {} public function formatted(): string { return number_format($this->amount / 100, 2) . " {$this->currency}"; } } ``` ```python from dataclasses import dataclass @dataclass(frozen=True) class Money: amount: int currency: str = "USD" def formatted(self) -> str: return f"{self.amount / 100:.2f} {self.currency}" ``` A `@dataclass` is Python's answer to [constructor property promotion](https://franktheprogrammer.com/articles/whats-new-in-php-8-0): it generates the initializer from the typed fields. `frozen=True` makes it `readonly`. Notice every method takes `self` as its first parameter, where PHP gives you `$this` implicitly. If you prefer the long form, it looks like this: ```python class Money: def __init__(self, amount: int, currency: str = "USD"): self.amount = amount self._currency = currency # a leading _ means "internal, please do not touch" ``` There is no real `private` or `protected`. A single leading underscore signals "internal," and a double underscore name-mangles to discourage access, but nothing stops a determined caller. Python's culture leans on convention where PHP leans on the compiler. ## Modern features you already use The recent additions to PHP that you reached for in the [modern SOLID](https://franktheprogrammer.com/articles/solid-principles-modern-php) era have direct Python analogues. `match` exists in both, and Python's version does even more (it can destructure): ```php $label = match ($status) { 200, 201 => "ok", 404 => "not found", default => "unknown", }; ``` ```python match status: case 200 | 201: label = "ok" case 404: label = "not found" case _: label = "unknown" ``` Enums map cleanly too. PHP 8.1's backed enums become an `Enum` subclass: ```php enum Status: string { case Active = "active"; case Closed = "closed"; } echo Status::Active->value; ``` ```python from enum import Enum class Status(Enum): ACTIVE = "active" CLOSED = "closed" print(Status.ACTIVE.value) ``` The one place PHP is more ergonomic is null handling. Python has no `?->` or `??`, so you write the check out: ```python name = "guest" if user is not None and user.profile is not None: name = user.profile.name ``` For a simple default, `value or fallback` works, but be careful: Python's `or` falls back on any falsy value (`0`, `""`, `[]`), not just `None`, so it is not a true null coalescing operator. ## Modules and imports PHP has namespaces, a `use` keyword, and an autoloader that maps class names to files. Python is simpler and more physical: a `.py` file is a module, a folder of them is a package, and you import by path. ```php // src/Billing/Money.php declares: namespace App\Billing; use App\Billing\Money; $m = new Money(500); ``` ```python # billing/money.py is the module "billing.money" from billing.money import Money m = Money(500) ``` There is no autoloader to configure and no `namespace` declaration inside the file. The directory structure is the namespace, and `from package.module import Thing` is your `use`. ## Idioms and gotchas A few things will trip you up coming from PHP, so here they are up front. **Naming.** PEP 8, Python's style guide, wants `snake_case` for functions and variables and `PascalCase` for classes. After years of PHP's `camelCase` methods this feels odd, but it is universal in Python. **The mutable default argument.** This is the classic trap. A default value is created once, not per call, so a mutable default is shared across every call: ```python def add_item(item, basket=[]): # BUG: every call reuses the same list basket.append(item) return basket def add_item(item, basket=None): # fix: default to None, build inside basket = basket if basket is not None else [] basket.append(item) return basket ``` **`is` versus `==`.** Use `==` to compare values (as you would in PHP) and `is` only for identity, in practice for `None` checks: `if x is None`. **Errors.** It is `try`/`except`/`finally`, not `catch`, and you `raise` rather than `throw`: ```php try { risky(); } catch (RuntimeException $e) { log($e->getMessage()); } finally { cleanup(); } ``` ```python try: risky() except RuntimeError as e: log(str(e)) finally: cleanup() ``` **Batteries included.** Python's standard library is broad. Reading JSON, handling dates, building paths, making HTTP requests, much of what you might pull a Composer package for is already there in modules like `json`, `datetime`, and `pathlib`. You already think in types, objects, immutability, enums, and pattern matching. Python just asks you to spell them with indentation and underscores. The fastest way to make it stick is to build something small and real, and the standard library makes that genuinely quick. ::note{label="Go further"} Put the new language to work: the [AWS Lambda series](https://franktheprogrammer.com/articles/package-a-python-lambda-as-a-docker-image) builds and deploys real Python functions as Docker container images, with everything tested locally first. :: # Return Type Declarations in PHP PHP 7 now makes it possible to declare return types for your methods. This allows you better control over the data that will be returned from each method in your application. The types of data you can return include but are not limited to the following. - self - array - callable - bool - float - int - string Apart from the above, you can also specify a type of Object that should be returned by passing the name of the Class or Interface. ::note{label="From 2026"} Return type declarations landed in PHP 7.0 and are standard today. PHP's type system has expanded a lot since: the current version is 8.4, which adds union return types, `mixed`, `never`, `void`, nullable types, and intersection types alongside the basics covered here. :: ## Example First let's specify the return type for our Calculator class. We do this by specifying the return type after the argument definition by appending a colon ":", followed by the return type. ```php firstName = $firstName; $this->lastName = $lastName; $this->age = $age; } public function name() : PersonName { return new PersonName($this->firstName, $this->lastName); } } class PersonName { private $firstName; private $lastName; public function __construct(string $firstName, string $lastName) { $this->firstName = $firstName; $this->lastName = $lastName; } public function full() : string { return $this->first() . ' ' . $this->last(); } public function first() : string { return $this->firstName; } public function last() : string { return $this->lastName; } } ``` ## Strict Typing Another option you have is to declare strict typing for your classes, which takes you from the default weak mode into a strong mode that forces the correct value to be returned rather than being coerced into the correct value. When the correct value type is not returned or passed, a TypeError exception will be thrown. Let's look at an example using strict mode. ```php sum(5, 10.2); } catch (TypeError $e) { echo $e->getMessage(); } ``` The above will output `15.2` as the result. We can pass any number of arguments above. Here's another example with a few more arguments. ```php sum(5, 10.2, 7, 9, 8); } catch (TypeError $e) { echo $e->getMessage(); } ``` This example outputs `39.2` as expected. Let's try and break this code now. We'll try by passing a string to see what happens. ```php sum(5, 10.2, 'test'); } catch (TypeError $e) { echo $e->getMessage(); } ``` If you run the above code you'll see that it is caught by our catch block with the following message: ```bash Argument 3 passed to Calculator::sum() must be of the type float, string given, called in CalculatorExample.php on line 17 ``` # Setting Up CocoaPods for FVM-managed Flutter Projects When working with Flutter and managing different versions using FVM, setting up CocoaPods can present a challenge. This guide provides a straightforward approach to installing CocoaPods for a specific Flutter version managed by FVM. ## Prerequisites - Ensure FVM is installed, and a specific Flutter version is set for your project. ## Installing CocoaPods with FVM 1. **Determine the Path**: Identify the path to the bin directory of the specific Flutter version managed by FVM. For instance, if you are using Flutter version 3.10.4, the path might look like this: `/Users//fvm/versions/3.10.4/bin`. 2. **Install CocoaPods**: Utilize the identified path to install CocoaPods as follows: ```bash sudo gem install -n /Users//fvm/versions/3.10.4/bin cocoapods ``` 3. **Navigate to Your Project's iOS Directory**: ```bash cd /ios ``` 4. **Install the Pods**: ```bash pod install ``` ::note{label="From 2026"} Newer Flutter and CocoaPods toolchains have eased much of this. If a fresh `gem install cocoapods` works against your FVM-managed Flutter version, you may not need the explicit `-n` path at all. :: # Setting Variables in Stylus ::note{label="Stylus series"} Part of my [Learning Stylus](https://franktheprogrammer.com/articles/learning-stylus-a-css-pre-processor) series. Stylus still works in 2026, but its ecosystem has largely moved on to Sass, PostCSS, and native CSS; the series intro has the full context. :: Unlike CSS, in Stylus you can assign expressions to variables that can be reusable throughout your stylesheets. ## Declaring Variables Let's assume we had a set of colors that we're going to be using throughout our document for headers, and text. **Stylus Code** ```stylus $primaryFontColor = #262626 $headerColor = darkblue body color: $primaryFontColor h1, h2, h3, h4, h5, h6 color: $headerColor ``` When creating variables, you do not need to include the $ dollar symbol. I use it here, as it stands out more to me letting me know that it is in fact a variable based on how variables are defined in the programming language I use from day to day. **Stylus Output** ```css body { color: #262626; } h1, h2, h3, h4, h5, h6 { color: #00008b; } ``` This is awesome. Now we can set base colors, or anything really to manage from 1 area. This will also help us keep on track to make sure we're sticking to our brand colors and styles. We may also want to set sizes for our primary headers and paragraphs. **Stylus Code** ```stylus $primaryHeaderFontSize = 4em $contentFontSize = 1em body font-size: 14px h1 font-size: $primaryHeaderFontSize p font-size: $contentFontSize ``` **Stylus Output** ```css body { font-size: 14px; } h1 { font-size: 4em; } p { font-size: 1em; } ``` ## Property Lookup/Reference Another amazing feature of Stylus is the ability of looking up a properties value without having to assign it to a variable. For instance, let's assume we want to create a box and center it to the page. **Stylus Code** ```stylus .box position: absolute top: 50% left: 50% width: 100px height: 100px margin-left: -(@width / 2) margin-top: -(@height / 2) background-color: blue ``` You'll see above that I was able to reference the width and height by simply typing @width or @height. How cool is that? **Stylus Output** ```css .box { position: absolute; top: 50%; left: 50%; width: 100px; height: 100px; margin-left: -50px; margin-top: -50px; background-color: #00f; } ``` # Setting Visibility for Your Class Constants in PHP Now in PHP 7.1+, you can set different visibility modifiers for each of your class constants. The available visibility modifiers consist of public, protected, and private. Any constants within your class set to protected or private that you try to access directly outside of the class will throw a Fatal Error. **Example Usage** ```php message = $message; } public function message() { return $this->message; } public function sendToEmail($email) { // Code Here } public function sendToSMS($mobileNumber) { // Code Here } } ``` The problem above is that not only are you creating the Notification message here, you're also expecting the same class to know how to send it to an Email or SMS. We should separate this business logic from the presentation data. Now we'll see how we can update it to better follow the Single Responsibility Principle. ```php message = $message; } public function message() { return $this->message; } } interface Notifier { public function send($to); } class EmailNotifier implements Notifier { private $notification; public function __construct(Notification $notification) { $this->notification = $notification; } public function send($to) { // Email Logic Here } } class SMSNotifier implements Notifier { private $notification; public function __construct(Notification $notification) { $this->notification = $notification; } public function send($to) { // SMS Logic Here } } ``` Now that we've organized the above in this fashion, it is easy for us to create new Notifiers without having to ever touch any of the existing classes. For instance, let's assume we want to create a notifier capable of sending notifications to Slack. We could simply create a new class for this like so. ```php notification = $notification; } public function send($to) { // Slack Logic Here } } ``` ## Open-Closed Principle (OCP) The entire idea of the Open-Closed Principle, is so that your classes should be easily extendible without the need of editing the classes directly. In our previous example regarding the Notifier you saw that we were able to easily add new Notification channels without ever having to modify any of the existing classes. This is exactly what this principle is trying to enforce. One thing you'll notice is that all of the Principles play together hand and hand. Let's look at another example of how this works by creating a trait for Vehicle with some predefined methods. We'll then create a new class for each body style while including the trait. You could also use an Abstract class to accomplish the same thing. ```php make; } public function model() : string { return $this->model; } public function year() : int { return $this->year; } } class Sedan { use Vehicle; public function __construct(string $make, string $model, int $year) { $this->make = $make; $this->model = $model; $this->year = $year; } } class Coupe { use Vehicle; public function __construct(string $make, string $model, int $year) { $this->make = $make; $this->model = $model; $this->year = $year; } } ``` Notice that now we can just continue making new body styles without ever modifying any of the existing classes. Let's make another body style for Convertible. ```php make = $make; $this->model = $model; $this->year = $year; } } ``` You see how simple it is now, to add additional body styles for our application. ## Liskov Substitution Principle (LSP) The idea behind the Liskov Substitution Principle is that a child class should never break its parent class. Instead we should be able to swap out any class of the same type with another while the application continues to work as normal. Let's add a bit more functionality to our Notification example from the first principle. ```php notifier = $notifier; return $this; } public function to(string $to) { return $this->notifier->send($to); } } class Notification { private $message; public function __construct($message) { $this->message = $message; } public function message() { return $this->message; } } interface Notifier { public function send($to); } class EmailNotifier implements Notifier { private $notification; public function __construct(Notification $notification) { $this->notification = $notification; } public function send($to) { // Email Logic Here } } class SMSNotifier implements Notifier { private $notification; public function __construct(Notification $notification) { $this->notification = $notification; } public function send($to) { // SMS Logic Here } } ``` You'll see that we added a Notify class that is capable of running a Notifier instance. Let me show you how we'd go about using this. ```php send(new EmailNotifier($notification))->to('fake@email-fake.com'); // Send SMS Notification $notify->send(new SMSNotifier($notification))->to('+19999999999'); ``` You see how easy it is to swap out the Notifier within our Notify class. This is amazing and should improve the ease of working within our code base. ## Interface Segregation Principle (ISP) Interface Segregation Principle states that no class should be forced to use methods it does not need. One way to solve this is to keep your interfaces, abstract classes, and traits small by creating multiple instead of a single large one. Let's take an example of a Paper Book. With a book you can flip to the next page and go to the previous page. If we're reading on a Kindle though, you'll be able to easily bookmark a page, apart from the regular page flipping functionality. This is how we may handle something like this. Keep in mind that this is a bare bones example, and there is still more that we would need to pass to actually have a functional system. Such as the actual book contents, and implementations of each method. I kept it short for the sake of this example. ```php notifier = $notifier; return $this; } public function to(string $to) { return $this->notifier->send($to); } } ``` You'll see that on our send method, we're passing the name of the interface Notifier which our Notifier concrete classes will implement. Doing this allows us to easily swap out the way we send notifications without ever having to modify any of the code within our Notify class. We were able to demonstrate this earlier by sending a SMS and Email Notification from the same Notify class. I've included the example again below. ```php send(new EmailNotifier($notification))->to('fake@email-fake.com'); // Send SMS Notification $notify->send(new SMSNotifier($notification))->to('+19999999999'); ``` ## Conclusion Following the above principles will improve your code base and make it easier for you and other developers to maintain and extend it. Don't worry if you're not following these 100%. They're only guidelines meant to help you improve the readability of your code, but not mandatory, if for some reason you do not like something about these principles, or do not agree with them. One thing I can guarantee though is that following these principles will definitely be beneficial in the long run. The choice is yours. # SOLID Principles in Modern PHP SOLID is a set of five object-oriented design principles, popularized by Robert C. Martin, that help you write code that is easier to extend, test, and maintain. The ideas have not changed in years. PHP, on the other hand, has changed enormously. When I first wrote about SOLID in 2016, expressing these principles took a lot of ceremony: manual property assignment, no real enums, getters and setters everywhere. PHP 8.5 strips most of that away, so the principles shine through with far less code. Let's walk through all five with modern PHP. ::note{label="PHP 8.5"} These examples target PHP 8.5, the current release as of 2026. They lean on features added across the 8.x line: constructor property promotion, enums, readonly properties, first-class callables, and property hooks with asymmetric visibility. Each of those has its own deep dive in my [What's New in PHP](https://franktheprogrammer.com/tags/whats-new) series. If you want to see how much has changed, the [original 2016 version](https://franktheprogrammer.com/articles/solid-principles-in-php) of this article is still up. :: ## Single Responsibility Principle A class should have one reason to change, which really just means it should do one job. The classic mistake is a class that both holds data and knows how to deliver it. Here the `Notification` holds the message, and nothing else. ```php final class Notification { public function __construct( public readonly string $subject, public readonly string $body, ) {} } ``` Because it is a readonly value object, once constructed it cannot be mutated, so there is exactly one reason it would ever change: the shape of a notification. Delivery is a separate concern, so it lives behind its own interface. ```php interface Channel { public function send(Notification $notification, string $to): void; } final class EmailChannel implements Channel { public function __construct(private readonly Mailer $mailer) {} public function send(Notification $notification, string $to): void { $this->mailer->deliver($to, $notification->subject, $notification->body); } } ``` Now if the way we model a message changes, we touch `Notification`. If the way we talk to an email provider changes, we touch `EmailChannel`. The two never bleed into each other. ## Open-Closed Principle Code should be open for extension but closed for modification. In practice that means you should be able to add behavior without editing the classes that are already written, working, and tested. Channels make this easy: adding Slack support is a new class, not a change to an old one. ```php final class SlackChannel implements Channel { public function __construct(private readonly SlackClient $slack) {} public function send(Notification $notification, string $to): void { $this->slack->post($to, "*{$notification->subject}*\n{$notification->body}"); } } ``` Nothing that already exists has to change. The same idea applies inside a class when you would otherwise reach for a sprawling conditional. An `enum` paired with `match` keeps the branches in one well-typed place, and adding a case is an additive change the compiler helps you complete. ```php enum Priority: string { case Low = 'low'; case Normal = 'normal'; case Urgent = 'urgent'; public function maxRetries(): int { return match ($this) { self::Low, self::Normal => 1, self::Urgent => 5, }; } } ``` If you add a `Priority::Critical` case later, `match` will throw on the unhandled value instead of silently falling through, so the gap is impossible to miss. ## Liskov Substitution Principle Anywhere you use a type, you should be able to swap in any of its subtypes without surprises. This is about behavior, not just matching signatures. Every `Channel` must genuinely honor the contract of `send()`: deliver the message, or fail loudly, but never silently pretend. A dispatcher can then treat them all the same. ```php final class Dispatcher { /** @param list $channels */ public function __construct(private readonly array $channels) {} public function dispatch(Notification $notification, string $to): void { foreach ($this->channels as $channel) { $channel->send($notification, $to); } } } ``` Because each channel upholds the same contract, the dispatcher does not need to know or care which ones it was handed. A channel that swallowed errors and returned as though it had delivered would technically satisfy the interface, but it would violate Liskov, and any caller relying on delivery would quietly break. ## Interface Segregation Principle No class should be forced to depend on methods it does not use. The fix is small, focused interfaces instead of one large one. A paper book and an e-reader can both turn pages, but only the e-reader can bookmark, so those are two different capabilities. ```php interface Pageable { public function nextPage(): void; public function previousPage(): void; } interface Bookmarkable { public function bookmark(): void; } final class PaperBook implements Pageable { public function nextPage(): void { /* turn the physical page */ } public function previousPage(): void { /* turn it back */ } } final class Ebook implements Pageable, Bookmarkable { public function nextPage(): void { /* render the next screen */ } public function previousPage(): void { /* render the previous screen */ } public function bookmark(): void { /* persist the reader's position */ } } ``` `PaperBook` implements only what it can actually do, with no empty `bookmark()` method left lying around. Since PHP 8.4, interfaces can even declare properties, including the hooks used to read them, so an interface can require a small slice of state without dragging in behavior a client does not need. ```php interface HasTitle { public string $title { get; } } final class Post implements HasTitle { public function __construct(private readonly string $headline) {} public string $title { get => trim($this->headline); } } ``` A collaborator that only needs a title can depend on `HasTitle` and nothing more. ## Dependency Inversion Principle High-level code should depend on abstractions, not on concrete implementations. Notice that `Dispatcher` already does this: it accepts `Channel` instances, never a specific `EmailChannel`. You wire the concrete pieces together at the edge of your application and inject them in. ```php $dispatcher = new Dispatcher([ new EmailChannel($mailer), new SlackChannel($slack), ]); $dispatcher->dispatch( new Notification('Welcome aboard', 'Thanks for signing up.'), 'frank@fjp.io', ); ``` Constructor property promotion makes the injection a single line, and marking the dependency `readonly` guarantees nothing swaps it out after construction. When you want a property the outside world can read but only the class can change, asymmetric visibility says exactly that. ```php final class ChannelRegistry { /** @var list */ public private(set) array $channels = []; public function register(Channel $channel): void { $this->channels[] = $channel; } } ``` The registry exposes its channels for inspection while keeping the only path to mutate them inside the class. High-level code depends on the `Channel` interface throughout, so you can swap email for Slack, or drop in a fake channel during a test, without touching the parts of the system that actually matter. ## Wrapping Up None of these principles are new, and none are specific to PHP. What modern PHP changes is how little code it now takes to follow them. Typed and readonly properties make intent explicit, promotion and enums cut the boilerplate, and property hooks with asymmetric visibility let small interfaces describe exactly what a collaborator needs. Even newer conveniences, like the pipe operator added in PHP 8.5, reward the same habit of building small, single-purpose units that compose cleanly. Treat SOLID as a set of guidelines, not laws. You will not always follow every one to the letter, and that is fine. But knowing them gives you a vocabulary for why one design feels easier to change than another. If you are curious how all of this looked a decade ago, the [2016 version](https://franktheprogrammer.com/articles/solid-principles-in-php) of this article works through the same ideas in the PHP of its day, and the contrast is its own little lesson. # Starting with Rust: From Installation to Your First Program This guide will walk you through installing Rust and writing your first "Hello, world!" program. By the end, you'll have a foundational understanding of the Rust toolchain and the Cargo.toml file's significance. ## Installing Rust The first step to starting with Rust is to install the Rust toolchain. This includes rustc (the compiler), cargo (the package manager and build tool), rustfmt (the code formatter), and rustup (the Rust toolchain installer and version management tool). 1. **Rustup**: The easiest way to install these components is through rustup, which manages Rust versions and associated tools. To install rustup, visit the [official Rust website](https://www.rust-lang.org/tools/install){rel=""nofollow""} and follow the instructions for your operating system. 2. **Verify Installation**: Once installed, open a terminal or command prompt and enter the following command to verify that rustc and cargo are installed: ```bash rustc --version cargo --version ``` ## Writing Your First "Hello, World!" Program With Rust installed, it's time to write your first Rust program. 1. **Create a New Project**: Open a terminal and run the following cargo command to create a new Rust project: ```bash cargo new hello_rust ``` This command creates a new directory called hello\_rust with a basic project structure. 2. **Explore the Project Structure**: Navigate into your project directory by `cd hello_rust`. You'll see two main files: - Cargo.toml: This file is the heart of your project's configuration. It defines your package, dependencies, and other metadata. - src/main.rs: This is your main source file where your Rust code lives. 3. **The Cargo.toml File**: Open the Cargo.toml file. Notice how it specifies the package name, version, authors, and more. Cargo uses this file to manage your project's dependencies and build settings. 4. **Writing the Program**: Open the src/main.rs file. By default, it contains a simple "Hello, world!" program: ```rust fn main() { println!("Hello, world!"); } ``` 5. **Running Your Program**: Back in the terminal, run the following command from your project directory: ```bash cargo run ``` This command compiles and runs your program, printing "Hello, world!" to the console. ## Understanding the Rust Toolchain - **rustc**: The Rust compiler, responsible for compiling Rust code into binary executables. - **cargo**: Rust's package manager and build tool, handling dependencies, compiling packages, and more. - **rustfmt**: Automatically formats Rust code according to style guidelines, ensuring consistent code style. - **rustup**: Manages Rust versions and associated tools, allowing you to easily switch between stable, beta, and nightly Rust. - **rust-analyzer**: An optional tool (usually an IDE plugin) that provides advanced code analysis and editor support for Rust. # Structuring Your Website With HTML 5 Semantics Prior to HTML 5, there was no real markup to help explain the intent behind your HTML code. The goal of HTML 5 was to offer a more readable way of writing your code, so that any author that comes after you can have an easier time going through what you've created. So what are semantics? Semantics is a process of providing meaning through a language. In the web, this means providing syntax that illustrates the meaning behind the content it wraps. Previously, we'd use divs for everything. It is now strongly suggested that you use divs as a last resort. Always try and use more semantic tags where appropriate. ::note{label="From 2026"} What I called HTML 5 back when I wrote this is simply HTML today, and every element here has universal browser support. The language kept growing, too: tags like `` for modal dialogs and `` for search regions have since joined the semantic toolbox. The advice holds up, reach for the tag that describes your content, and fall back to `
` only when nothing more meaningful fits. :: ## Sectioning Elements As defined in the HTML 5 specification, Sectioning content defines the scope of headings and footers. The following tags are used to define sections within your document. - article - main - aside - nav - section - header - footer Each element has a different meaning though as to what kind of data is expected. ### Article Element The article element, you can think of as a self-contained element. Meaning that if you were to remove the element with its containing content, that the rest of the document would still work just fine. Examples of this could be a blog post, a product on a category page, or really any other stand alone item of content. You can also nest article elements within other article elements. When nesting article elements, the inner article elements would be considered elements related to the parent article element. For instance, comments on a blog post. ### Main Element When you have a known area of content that is considered the primary content of the page, you should mark it up with the main element instead of article. You can still use article as well, but it would be considered redundant. ### Aside Element The aside element should be used to surround a section of the document that contains information that is related to the sibling content, but not directly about it. For instance, you may see sidebars as a common use case where the side bar contains related links regarding the primary information on the page. Another common use case for the aside tag would be with advertisements. For instance in the case that you have some ad space on your page that is related to the information you have posted, but you want it to be kept separate from the main content. ### Nav Element The nav link should be used to surround a section of a page that contains links to other pages or areas of the same page. For instance, your top navigation for your website. One thing to note though is that the nav element should not go around all groupings of links on your pages. Instead you should only use the nav tag to surround major navigational blocks. Also, let's assume you have your categories within your primary navigation of the website. If you also have those same categories in your footer, then there is no need to wrap them in the nav element. ### Section Element The section element is one that is commonly confused with article. The section element should only be used to represent a generic section of a page. Think of it this way, if there is content on your page that should be syndicated then use article, if not you can use section. For instance, you may want to split up your homepage into sections, since each section of a homepage is typically generic. ### Header Element The header element is used for grouping the following information: 1. Branding Information 2. Heading Information 3. Navigational Aids Not only can this tag be used for the header of your website, but it is also used to mark the introductory areas of each of your sections. For instance, let's assume we have a category page with products on it. On our category page we have our category name, category short description, and maybe some related category links. You could essentially group this section with our header, as it's the start of our category page. Header tags can be used within sectioning elements or sectioning root elements. ### Footer Element You can have multiple footer elements, as you can with header elements. The footer element represents the footer for its nearest ancestor. It normally contains information like: - Author Information - Copyright Information - Related Links ## Sectioning Roots Sectioning roots are used to group sets of information that are not needed within the document outlines of its ancestors. In fact, any markup placed within a sectioning root element is completely ignored by its parent document structure. Below are the tags considered as sectioning root elements. - blockquote - body - fieldset - figure - td ## Testing Your Structure A nice browser tool that you can use in either Firefox or Chrome is *HeadingsMap*. This extension allows you to see your current document outline in both the latest HTML 5 overview, as well as the previous HTML 4 outline structures. ### Features Include - Shows structure of the current document's headers. - Displays the outline of each of the headers. - Alerts you if there is a section without a header. # Support for keys in list(), or its new shorthand syntax [] in PHP Now as of PHP 7.1, you can define the keys of your array that will be parsed when destructuring your arrays. Prior to PHP 7.1, you could only use arrays with numeric indexes. Now with this new addition, our lives just got easier. Let's go over a few examples below on how we can use this new feature. **Example** Let's start by setting up our array. ```php 1, 'age' => 29, 'name' => 'Frank', ], [ 'id' => 2, 'age' => 28, 'name' => 'Sarah', ], [ 'id' => 3, 'age' => 27, 'name' => 'Michael', ], ]; ``` Now that we have our array setup, we'll want to start by destructuring the first person in the array. ```php $id, 'name' => $name, 'age' => $age) = $person; echo 'ID: ' . $id . '
'; echo 'Name: ' . $name . '
'; echo 'Age: ' . $age . '
'; ``` You'll notice that I was able to specify the keys above, and also that they do not need to be in the same order as the original array. Since our keys match up with the keys in our array, our application is now smart enough to handle that. Let's rewrite the above to use the shorthand syntax now. ```php $id, 'name' => $name, 'age' => $age] = $person; echo 'ID: ' . $id . '
'; echo 'Name: ' . $name . '
'; echo 'Age: ' . $age . '
'; ``` ::note{label="From 2026"} Keyed destructuring has been baseline since PHP 7.1, and the `[]` shorthand is the form you will see most often today. You can also nest it, for example `['name' => $name, 'address' => ['city' => $city]] = $person;`, and use it directly in a `foreach` to unpack each row. :: # Symmetric Array Destructuring in PHP As of PHP 7.1, you can now use the shorthand array syntax to destructure your arrays for assignment. Previously you would have had to use a function like list, but now you can use the simple new array shorthand syntax. ::note{label="From 2026"} This was new in PHP 7.1 back in 2016. The shorthand `[...]` destructuring syntax is now standard and works everywhere on modern PHP (8.4 at the time of writing). :: Let's look at some examples. **Basic Example** ```php '; } ``` # The grill-me Skill for Claude Code You have probably hit this one. You type a paragraph describing some feature, Claude reads it, and half a minute later it is off building. The thing it builds answers the prompt you wrote, not the thing you actually had in mind. The requirements were fuzzy in your head, so they came out fuzzy on the keyboard, and the model just filled the gaps with reasonable-looking guesses. `grill-me` turns that around. Rather than you briefing Claude so it can start coding, Claude interviews you first. It pulls your plan apart one question at a time until there is nothing left that is vague enough to build wrong. I went in expecting a gimmick and came out using it most weeks. ## What grill-me actually is It is a Claude Code *skill*, which is just a small `SKILL.md` file that Claude finds and loads by itself when the moment fits. Matt Pocock wrote it (you may know him from Total TypeScript), and it lives in his public [`mattpocock/skills`](https://github.com/mattpocock/skills){rel=""nofollow""} repo, which is basically a dump of the skills from his own `.claude` directory. The skill is community-made, not something Anthropic ships, and it took off. The repo collected tens of thousands of stars after he posted about it, and `grill-me` was the one everyone fixated on. Partly that is because it names a problem people run into constantly, and partly because there is almost nothing to it. ## The whole skill is three sentences That last point is worth dwelling on. No scripts, no scaffolding, no tooling. The entire skill is a description and three sentences of instruction: ```markdown --- name: grill-me description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me". --- Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer. Ask the questions one at a time. If a question can be answered by exploring the codebase, explore the codebase instead. ``` That is the whole file, sitting at `skills/productivity/grill-me/SKILL.md`. All the work is being done by the prompt. The line that carries the weight is "walk down each branch of the design tree." It tells Claude to treat your feature as a tree of decisions, where every choice opens up a few more, instead of one block of text to skim and act on. The other two sentences just keep things sane: ask one question at a time, and go read the code rather than asking when the repo already knows the answer. ## What it feels like to use You kick it off by saying "grill me" (or running `/grill-me`) next to whatever you want torn apart. Then the roles swap. Claude starts asking, and it does not run out of questions at the convenient point. A few details make it work: - **One question at a time.** You are not handed twenty bullet points to clear in one go. Each question gets its own turn, so you have to actually think about it rather than skim past. - **It brings its own answer.** Every question comes with the option Claude would pick and why. Reacting to a concrete proposal is a lot easier than producing one from nothing, which is what keeps the session moving. - **It reads the code first.** When the repo can settle a question, Claude goes and looks instead of making you recite something that is already written down. - **It keeps going.** It works down the branches until you genuinely agree on the plan, not until the conversation feels long enough. The annoying questions are the useful ones. "What happens when this fails halfway through?" is exactly the kind of thing you were quietly planning to deal with later. Answering it before any code exists is far cheaper than running into it three files deep. ## Installing it The fastest way is Pocock's installer, which grabs the skills from the repo and lets you choose what to add: ```bash npx skills@latest add mattpocock/skills ``` If you would rather skip the installer, remember a skill is just a file in a folder. Save the `SKILL.md` above to `~/.claude/skills/grill-me/SKILL.md` and Claude Code will find it on its own, then call it with `/grill-me`. Every skill works the same way, a `SKILL.md` sitting in its own folder under `~/.claude/skills/`. If you want to see how skills and plugins hang together, I wrote up [how to build a Claude Code plugin](https://franktheprogrammer.com/articles/how-to-build-a-claude-code-plugin) separately. ## Beyond coding, and grill-with-docs None of this is really about code. Any time your thinking is messier than you want to admit, whether it is a product call, a piece of writing, or an architecture you cannot quite commit to, being walked down the decision tree drags the fuzzy bits into the light. Pocock has said he uses it well outside of programming, which makes sense once you notice it is a thinking tool that happens to be wearing a coding hat. For codebases there is a follow-up called `grill-with-docs`. It grills you the same way, but as decisions firm up it writes them down: a `CONTEXT.md` glossary of your project's terms, plus Architecture Decision Records dropped in as you go. So the grilling stops being a single conversation and starts to pile up into shared context you keep between sessions. There are community forks too (`Jekudy/grillme-skill` is one), which tends to happen when an idea lands. ## Worth it? For anything bigger than a one-liner, yes. The cheapest tokens you spend are the ones on planning, and the most expensive bug is the one you confidently build from a vague brief. `grill-me` is a structured way to pay the small cost first. If that is your kind of thing, [getting more out of Claude Code](https://franktheprogrammer.com/articles/getting-more-out-of-claude-code) goes deeper on prompting and token economy for the same basic reason: sort the plan out before you spend the budget building it. # Troubleshooting Xcode 15 Build Issues in Flutter Projects Updating to Xcode 15 brought a few hurdles in my Flutter project, especially since I was using an older version of CocoaPods, below version 1.13. Here's a breakdown of how I navigated through these issues and got my project back on track. ## Step 1: Update Podfile (Flutter / Xcode 15) Update your Podfile with the following snippet: ```ruby post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| xcconfig_path = config.base_configuration_reference.real_path xcconfig = File.read(xcconfig_path) xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } end end installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' end end end ``` ## Step 2: Update Build Phases 1. Open Xcode 2. Click "Runner" in the project navigator 3. Navigate to Targets *(Runner in the sidebar)* 4. Click on Build Phases *(found in the top bar)* 5. Move "Embed App Extension" above the Run Script step ## Step 3: Run Flutter Clean Once the above changes are done, clean your project with the following command: ```bash flutter clean ``` You should now be able to successfully build and run your project. ::note{label="From 2026"} These workarounds are specific to the Xcode 15 / older-CocoaPods era of 2023. Newer Flutter and Xcode toolchains have eased most of this, so on a current setup you likely won't need the `DT_TOOLCHAIN_DIR` Podfile patch. :: # Type Hinting Callable Functions in PHP As of PHP 5.4, you can type hint your method arguments with the callable keyword allowing you to enforce the type of data that is passed via your arguments. Let's look at an example where we create a Sort class, with a custom method that takes a callable function so that we can customize how our data is sorted. **Example** ```php $number2; }); ``` That's all there is to it. Now we can pass any function as a second argument to our method allowing us to pass any custom sorting algorithm to our class. # Type Hinting with Nullable Types in PHP As of PHP 7.1, you can now set your type declarations as nullable by simply prefixing them with a question mark ?. In doing so a null value can be passed in as a parameter or returned as a value for your methods. Let's look at an example where we setup a simple product class that can pass a name via the constructor. We will type hint the variable to force the use of a string, but we'll also set it as a nullable type this way a null value can be passed. ```php name = $name; } public function name() : ?string { return $this->name; } } ``` Now let's look at how we would use the above to pass a nullable value to the constructor. ```php name()); ``` The above code would output a response of *NULL*. # Type Hinting With The Iterable pseudo-type In PHP As of PHP 7.1, you can now type hint your method/function arguments with the keyword iterable for handling arrays or even objects that implement the Traversable interface. Let's create an example where we can convert any string to a slug. We'll have 2 methods so that we can pass either a string or iterable data. We'll start by setting up our SlugGenerator class. ```php titles, $title); } public function getIterator() : Iterator { return new ArrayIterator($this->titles); } } $titles = new Titles; $titles->add('Test Title 1'); $titles->add('Test Title 2'); $titles->add('Test Title 3'); $slugs = SlugGenerator::byArray($titles); // Outputs array of slugs ``` You'll see that we're able to use the same SlugGenerator class without having to modify our original class. Awesome, right? # Using Browser Sync with Gulp for Live Reloading Browser Sync is a nice tool to use while developing. It allows your browser to reload live, when changes are made to your files. For instance, assuming we're watching our CSS file for changes we can have the browser auto refresh/sync when it sees those changes made. In this article, I'll show you the basics of how to get started with Browser Sync. Feel free to read up on the documentation for a more in depth look at all of the configuration options available. [Browser Sync Documentation](https://www.browsersync.io/docs){rel=""nofollow""} ::note{label="From 2026"} Modern dev servers like [Vite](https://vite.dev/){rel=""nofollow""} ship with built-in live reload and hot module replacement, so a separate BrowserSync plus Gulp setup is rarely needed today. :: ## Installation Steps First thing we'll need to do is setup our project. For this project we'll be using gulp along with browser-sync for our packages. Let's start by installing gulp to our project. ```bash yarn add gulp ``` Next, we'll want to install the browser-sync plugin. ```bash yarn add browser-sync ``` ## Configuration Now that we have our 2 plugins installed, let's go ahead and configure our gulpfile.js file. We'll first want to include the 2 packages that we recently installed. ```javascript var gulp = require('gulp'); var browserSync = require('browser-sync').create(); ``` Next, let's setup a task called watch, which will look for changes in our specified CSS file. ```javascript gulp.task('watch', function() { browserSync.init({ proxy: "bootstrap.dev" }); gulp.watch("css/*.css").on('change', browserSync.reload); }); ``` You'll note above that I have setup a local domain for testing called bootstrap.dev. The proxy basically lets browser-sync know that it should load the website from that given domain. From the example above, you'll see that we need 2 parts for this to work. 1. We need to initiate browser sync. 2. After we initiate it, we'll then want to set our watcher on our CSS file, and let it know when something changes to reload the browser. You can monitor changes on any files you'd like, for instance HTML or even JavaScript. ## Testing Once we're ready to test, we can simply run the command gulp watch to see our changes take effect. We can now edit our CSS file and see the changes happen live within our browser. When you run gulp watch, you'll notice that our browser automatically opens up with the website connected to our browser sync instance. ![Browser Sync Example](https://franktheprogrammer.com/storage/posts/dQJlFemJVb47vpujl7cvivc4LGcfG3xy4EHyihc3.gif) There are many more configuration options available to use, and I urge you to go through the documentation so that you can see fully how Browser Sync can improve your development process. # Using CSS Transitions CSS transitions are the standard way to apply transitions to your elements, and have been for years, replacing the old approach of using JavaScript. In this article, I'll go through each of the transition properties available, and provide examples of how to use them. Below is a list of browser prefixes that were once used to enable backward compatibility with older browsers for each of the properties we will be discussing. **Browser prefixes:** - -webkit- , provides backward support for webkit browsers such as Chrome. - -moz- , provides backward support for Firefox. - -ms- , provides backward support for I.E. - -o- , provides backward support for Opera. ::note{label="From 2026"} You can skip almost all of these today. Every current browser supports `transition` and the properties below without a prefix, so the bare version is all you need. The `-moz-`, `-ms-`, and `-o-` prefixes are obsolete, and even `-webkit-` is rarely necessary for transitions. If you ever do need to support ancient browsers, let a build tool like Autoprefixer add the prefixes for you instead of writing them by hand. I've kept the prefixed example below for reference. :: There are a handful of properties that we will be using for this article. One is considered the short hand version, where you can combine the values of each property into one line. **The available transition properties are:** - transition (short hand) - transition-duration - transition-property - transition-delay - transition-timing-function We'll discuss the shorthand version at the end, that way I can show you how each individual property works and how we can bring them together later. ## Transition Duration The transition-duration property is super important, for transitions to work in general. Without setting the duration, you'd never see any of the transitions since the default value is 0. This property specifies how many seconds (s) or milliseconds (ms) a transition effect should take to complete. ## Transition Property The transition-property property allows you to specify the name of a CSS property that the transition should be applied to. For instance, if you wanted to apply a transition to the width property, then you'd simply use width as your value for this property. You can also apply transitions to multiple properties by using a comma to separate them. Below is a list of possible properties you may use. - none: This disables the transition effect for your element. - all : This is the default value, and lets our document know to look at all properties for transition changes. - property: You can specify the properties you want to apply transitions to by separating them by comma. - initial : This one will set the property to its default value. - inherit : Use this one, to inherit the transition property from its parent element. ## Example Usage for Transition Property and Transition Duration Let's assume we have a box, that we'd like to change the width and height of using a transition on hover. **Let's look at our HTML first.** ```html
``` Next we'll look at setting up our CSS without the backward browser support. ```css .box { width: 150px; height: 150px; background-color: blue; transition-property: width, height; transition-duration: 2s; } .box:hover { width: 450px; height: 250px; } ``` And that's all you need. When you open the browser now and hover over the box you've created, you'll see that it now animates to the new width and height on hover. How cool is that? Let's look at the CSS with the backward browser support now for your reference: ```css .box { width: 150px; height: 150px; background-color: blue; -webkit-transition-property: width, height; -moz-transition-property: width, height; -ms-transition-property: width, height; -o-transition-property: width, height; transition-property: width, height; -webkit-transition-duration: 2s; -moz-transition-duration: 2s; -ms-transition-duration: 2s; -o-transition-duration: 2s; transition-duration: 2s; } .box:hover { width: 450px; height: 250px; } ``` For the next examples, I will not be including the backward browser support versions in order to keep the code examples small. Keep in mind though, that you'll want to use the prefixed versions in your actual code base for usability and consistency across various browser versions. ## Transition Delay Let's assume now that you do not want the animation to start right away. Instead you want to add a delay of 500 milliseconds. Luckily this is easy to accomplish with the transition-delay property. Let's have a look by continuing on our previous example with the box styles. ```css .box { width: 150px; height: 150px; background-color: blue; transition-property: width, height; transition-duration: 2s; transition-delay: 500ms; } .box:hover { width: 450px; height: 250px; } ``` Now when you refresh your browser and hover over your box, you'll see that there is a slight delay before the animation starts. AMAZING! ## Transition Timing Function Now the transition-timing-function property allows you to specify the speed at which the transition changes over the duration of time. This is great to make the transitions seem a bit more real. Here's the supported list of values for this property below. - ease: This is the default behavior. The animation will start slower, then speed up, and end slowly. - linear: The animation will maintain the same speed from start to end. - ease-in: The animation will start slow and then speed up. - ease-out: The animation will start fast and then slow down. - ease-in-out: The animation will start slow and end slow, but speed up in between. - step-start: Alias to steps start value. - step-end: Alias to steps end value. - steps(int,start|end): Steps defines the number of steps it will take for a transition to complete. The first parameter is where you define the number of steps, and the second parameter defines when the values of a property should change. Either at the start of the transition step, or at the end, after the step completes. - cubic-bezier(n,n,n,n): This one gives you even greater control of the transition. Each n value should be numeric between and including 0 to 1. - initial: This will set the property to its default value. - inherit: This will inherit the value of its parent element. Let's look at some examples. First we'll have our box resize using the linear value. In doing so you'll notice that the speed of the transition is consistent throughout the entire duration of the transition. ```css .box { width: 150px; height: 150px; background-color: blue; transition-property: width, height; transition-duration: 2s; transition-delay: 500ms; transition-timing-function: linear; } .box:hover { width: 450px; height: 250px; } ``` Let's look at an example using steps now. Steps allows us to define how many steps it will take until finishing the transition. ```css .box { width: 150px; height: 150px; background-color: blue; transition-property: width, height; transition-duration: 2s; transition-delay: 500ms; transition-timing-function: steps(6, end); } .box:hover { width: 450px; height: 250px; } ``` Here we're using steps(6, end), which lets our transition know that it should complete in exactly 6 steps. We passed end as our second parameter, letting our transition know that each step should change the values at the end of the interval instead of the start. ## Transition: The Shorthand Version Now that we've covered each of the different property types available for transition, Let's look at how we can shorten our code using the short-hand version. Here's a break down of the transition property and in what order each property value should go. - transition: property duration timing-function delay; We'll take our same styles from the box example with the linear transition and convert it to the short hand version. ```css .box { width: 150px; height: 150px; background-color: blue; transition: width 2s linear 500ms, height 2s linear 500ms; } .box:hover { width: 450px; height: 250px; } ``` You'll see above that we were able to separate each transition properties by a comma in order to affect width and height. The code is now much shorter. # Using Functions and Mixins with Stylus ::note{label="Stylus series"} Part of my [Learning Stylus](https://franktheprogrammer.com/articles/learning-stylus-a-css-pre-processor) series. Stylus still works in 2026, but its ecosystem has largely moved on to Sass, PostCSS, and native CSS; the series intro has the full context. :: Stylus allows you to create functions and mixins of reusable code for your stylesheets. You can also handle mathematical operations, unary operations, and more allowing you complete control over your stylesheets with ease. Mixins are synonymous with functions. What determines when you call something a mixin or function is based on its usage. Mixins by definition are invoked as a statement, where functions are part of an expression. ## Constructing A Function / Mixin Let's assume you're wanting to create a function to handle rounding corners of an element. We can simply name it border-radius, to keep it in line with the actual syntax. Only now when we'd call border-radius within our code it would generate the code with support for various browsers. Let's create a box as our example with rounded corners. **Stylus Code** ```stylus border-radius() -webkit-border-radius: arguments -moz-border-radius: arguments border-radius: arguments .box width: 100px height: 100px background-color: blue border-radius(10px) ``` **Stylus Output** ```css .box { width: 100px; height: 100px; background-color: #00f; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } ``` Notice how the output now contains the 3 lines required to generate rounded corners for multiple browser support. Another thing to note is that we were able to pass the value of 10px to each line by calling the arguments keyword. This will allow us to reuse the function throughout our entire code base. We can even take this a step further, and make the function a little more transparent by losing the parenthesis altogether. This is the beauty of using mixins, as it allows you to transparently add support for various browsers while keeping the syntax the same. **Stylus Code** ```stylus border-radius() -webkit-border-radius: arguments -moz-border-radius: arguments border-radius: arguments .box { width: 100px height: 100px background-color: blue border-radius: 10px } ``` **Stylus Output** ```css .box { width: 100px; height: 100px; background-color: #00f; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } ``` Notice we still get the same result, and it's a little more natural writing it this way. Let's look at another example of creating a mixin where we alternate the background colors of our rows of a table. **Stylus Code** ```stylus stripeRows(even = #eeeeee, odd = #dddddd) tr background-color: odd &:nth-child(even) background-color: even table stripeRows() ``` **Stylus Output** ```css table tr { background-color: #ddd; } table tr:nth-child(even) { background-color: #eee; } ``` ## Argument Defaults You can also set default values for your arguments if one is not provided. Let's look at the border-radius example, and add a sensible default if a value is not provided. **Stylus Code** ```stylus border-radius(radius = 10px) -webkit-border-radius: radius -moz-border-radius: radius border-radius: radius .box width: 100px height: 100px background-color: blue border-radius() ``` Notice that we set the default value to 10px, so if no value is set the border radius would be set to 10px by default. ## Named Parameters You can also pass named parameters to your function as well. The benefit to this approach is if you have more than one parameter to your function you're not required to remember the order of how the arguments should be passed. **Stylus Code** ```stylus .box width: 100px height: 100px background-color: blue border-radius(radius: 10px) ``` ## Multiple Return Values Interestingly enough, a function can also return multiple values as well. Let's look at an example where we create a perfect square box. **Stylus Code** ```stylus squareBox(x, color = blue) dimensions = squareCoordinates(x) box(dimensions[0], dimensions[1], color) squareCoordinates(x) return x x box(x, y, color = blue) width: x height: y background-color: color .box squareBox(150px) ``` **Stylus Output** ```css .box { width: 150px; height: 150px; background-color: #00f; } ``` You'll notice that now we only have to call the function squareBox() and pass the size, and the rest of the box code will be generated for us. ## Conditionals Like other programming languages, we can also take advantage of conditionals. This can be useful to us in cases where we'd need some set of styles applied in certain cases and not others. Let's look at an example where if someone creates a box which is not a perfect square we add some extra margin to the needed side to make it square. **Stylus Code** ```stylus box(x, y, color = blue) width: x height: y background-color: blue if(x > y) margin: ((@width - y) / 2) 0 else margin: 0 ((@height - x) / 2) .box box(50px, 100px) float: left ``` **Stylus Output** ```css .box { width: 50px; height: 100px; background-color: #00f; margin: 0 25px; float: left; } ``` ## Anonymous Functions You can also create anonymous functions using the @(){} syntax. The [Stylus documentation on functions](https://stylus-lang.com/docs/functions.html){rel=""nofollow""} has a good example built around a sort function. ## Arguments The arguments keyword is available within each function block. It will contain the list of all arguments passed to the function. In one of our previous examples of the border-radius, you found that we were using the keyword arguments instead of a variable name. This allowed us to pass any parameters/arguments we need to our function without manually specifying each allowed argument . **Stylus Code** ```stylus border-radius() -webkit-border-radius: arguments -moz-border-radius: arguments border-radius: arguments .box { width: 100px height: 100px border-radius(10px) } ``` **Stylus Output** ```css .box { width: 100px; height: 100px; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } ``` # Using Selectors in Stylus ::note{label="Stylus series"} Part of my [Learning Stylus](https://franktheprogrammer.com/articles/learning-stylus-a-css-pre-processor) series. Stylus still works in 2026, but its ecosystem has largely moved on to Sass, PostCSS, and native CSS; the series intro has the full context. :: Selectors are a way to pick the elements that you want styled. In Stylus, similar to CSS, you can apply a set of styles to any element by separating them by a comma delimited list. Stylus though, also allows you to select multiple elements by separating each on their own line. Let's look at an example of each to see how this works. **Stylus (comma delimited)** ```stylus b, .strong color: green font-style: italic ``` **Stylus (newline delimited)** ```stylus b .strong color: green font-style: italic font-weight: bold ``` **Compiled CSS (same output for both examples above)** ```css b, .strong { color: #008000; font-style: italic; font-weight: bold; } ``` ## Parent Selector You can also reference the parent elements for a particular style by using the & ampersand symbol. For instance, let's assume you want to add a hover for color changes to some elements. If you need to use the ampersand symbol within a selector without it referencing the parent, then you can use the backslash character to escape it. & **Stylus Code** ```stylus textarea, input color: lightgray &:hover color: gray ``` **Stylus Output** ```css textarea, input { color: #d3d3d3; } textarea:hover, input:hover { color: #808080; } ``` ## Partial Reference Partial selectors ^ [N] can be used anywhere within your selectors allowing you to reference a specific level of the parent where N is your numeric level starting at 0, which is the highest element within your chain. When rendered, they contain the entire chain of your selectors until the specified nested level. If you use a positive number then the higher the number, the closer to your actual element in the chain you're styling you are. If you use a negative number, it would be the complete reverse. Instead of starting from the first element in your chain, you would start from the current element and reversely traverse the chain. They are especially useful when using them within your mixins when you are unaware of the nesting level your mixin may be called from. Let's look at an example to see how this can be used in application. We'll start by setting up our HTML **HTML Code** ```html

Page Title Here

``` Now, let's make it so that when you hover over any part of the title that the word "Here" changes from one color to another. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen ^[0]:hover & color: greenyellow ``` You'll see that we were able to select ".page-header" and state that on hover of that element apply the color change to the element `&--highlight-word`. **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; } .page-header:hover .page-header--highlight-word { color: #adff2f; } ``` ## Initial Reference A shortcut really to ^ [0], the initial reference \~/ can only be used at the start of your selector. We can update our example code above to take advantage of this shortcut like so. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen ~/:hover & color: greenyellow ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; } .page-header:hover .page-header--highlight-word { color: #adff2f; } ``` ## Relative Reference The relative reference ../ can also only be used at the start of your selectors. They can be used to go as far up the chain as you'd like. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen ../:hover & color: greenyellow ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; } .page-header:hover .page-header--highlight-word { color: #adff2f; } ``` ## Root Reference The root selector /, is a reference to your document root. For instance, let's assume you had a set of styles that you also wanted to assign to a root level class. In our example we'll create a class .hover-color, which does just that. On hover it changes the color of the text it is applied to. We'll just append to our existing code to show off how to do this. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen ~/:hover &, /.hover-color:hover color: greenyellow ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; } .page-header:hover .page-header--highlight-word, .hover-color:hover { color: #adff2f; } ``` ## selector() and selectors() There are 2 functions available to you as well that allow you to get the currently rendered selectors from the current chain. Here's an example using the selector() function. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen content: selector() ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; content: '.page-header--highlight-word'; } ``` You'll see that the content is now filled with the current rendered class name. If we were to change the above from selector() to selectors(), you'd see that all of the classes from the chain would appear. **Stylus Code** ```stylus .page-header color: darkgreen &--highlight-word color: lightgreen content: selectors() ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; content: '.page-header', '&--highlight-word'; } ``` You can also use the selector() function to create your nested selector structure. **Stylus Code** ```stylus { selector('.page-header') } color: darkgreen { selector('.page-header', '&--highlight-word') } color: lightgreen { selector('.page-header', 'span') } font-weight: bold ``` **Stylus Output** ```css .page-header { color: #006400; } .page-header--highlight-word { color: #90ee90; } .page-header span { font-weight: bold; } ``` # Void Return Types in PHP As of PHP 7.1, we can now use void return types within our methods. This is useful for cases where you have methods that are just setting or processing data without the need of returning any values. Functions or Methods declared with void as their return type would simply use an empty return statement or not use a return statement at all. Let's look at an example below where we create a Product class capable of setting a product's name through the constructor or a setName method. The setName method would have a return type of void, since it's only setting a value and not set up to return anything. ```php name = $name; } public function setName(string $name) : void { $this->name = $name; } public function name() : string { return $this->name; } } ``` You could also just set a blank return statement like so. ```php public function setName(string $name) : void { $this->name = $name; return; } ``` # What's New in PHP 8.0 PHP 8.0 landed on 26 November 2020, and unlike the 7.x line it really earned the major version bump. It is the release that brought the JIT compiler, but the changes you actually feel day to day are the language features: less boilerplate, stricter types, and a handful of constructs that make everyday code read better. Let's walk through the ones I reach for most. ## Constructor property promotion For years, declaring a value object meant writing every property three times: once as a field, once as a constructor parameter, once as an assignment. Promotion collapses all three into the parameter list. ```php final class Money { public function __construct( public readonly int $amountCents, public readonly string $currency = 'USD', ) {} } $price = new Money(1999, 'EUR'); echo $price->amountCents; // 1999 ``` The `public readonly` in front of each parameter declares the property, sets its visibility, and assigns it for you. A class that used to be fifteen lines is now five, and there is nowhere for a typo between the parameter and the field to hide. ## Named arguments Named arguments let you pass values by parameter name instead of position. They pair beautifully with functions that have a long tail of optional settings, because you can skip straight to the one you care about. ```php function createUser( string $email, bool $active = true, bool $verified = false, string $role = 'member', ): User { // ... } $user = createUser('frank@fjp.io', role: 'admin', verified: true); ``` No more passing `true, false, true` and counting commas to remember which flag is which. The call site documents itself, and you only name the arguments you are changing from their defaults. ## The match expression `match` is like `switch`, but it is an expression that returns a value, it compares with strict `===`, and it has no fall-through. That combination kills a whole category of bugs. ```php $label = match ($response->status) { 200, 201, 204 => 'Success', 301, 302 => 'Redirect', 404 => 'Not Found', 500 => 'Server Error', default => 'Unknown', }; ``` Each arm is a single expression, multiple values share an arm with a comma, and an unmatched value with no `default` throws instead of silently doing nothing. If you have used the [spaceship operator](https://franktheprogrammer.com/articles/php-spaceship-operator) or the [null coalescing operator](https://franktheprogrammer.com/articles/php-null-coalescing-operator), this is the same spirit: small syntax that removes ceremony. ## The nullsafe operator Reaching through a chain of objects that might be null used to mean a ladder of `if` checks or nested ternaries. The nullsafe operator `?->` short-circuits the whole chain to `null` the moment any link is missing. ```php $country = $user?->getAddress()?->country; ``` If `$user` is null, or `getAddress()` returns null, the expression stops and yields `null` rather than throwing on a method call against nothing. It reads top to bottom, exactly the way you would describe it out loud. ## Union types PHP 8.0 made union types a native, runtime-checked part of declarations. You can now say a value is one of several types directly in the signature, rather than dropping to a docblock and hoping callers read it. ```php function pad(int|string $value, int $width): string { return str_pad((string) $value, $width, '0', STR_PAD_LEFT); } pad(7, 4); // "0007" pad('42', 4); // "0042" ``` This builds on the [scalar type hints](https://franktheprogrammer.com/articles/scalar-type-hints-php) and [return type declarations](https://franktheprogrammer.com/articles/return-type-declarations-in-php) PHP gained earlier. Where you used to write `?int` to mean "int or null" (see [nullable types](https://franktheprogrammer.com/articles/type-hinting-with-nullable-types-in-php)), you can now express far richer combinations, and the engine enforces them. ## String helpers that read like English Checking what a string contains used to mean `strpos($haystack, $needle) !== false` and remembering that `0` is a valid position. PHP 8.0 finally shipped the three functions everyone had been hand-rolling. ```php $file = 'invoice-2020.pdf'; str_contains($file, '2020'); // true str_starts_with($file, 'inv'); // true str_ends_with($file, '.pdf'); // true ``` They return a clean boolean, so no more `!== false` dance. As a bonus, `throw` became an expression in 8.0, so you can guard a value inline: ```php $config = $options['dsn'] ?? throw new InvalidArgumentException('Missing dsn'); ``` PHP 8.0 set the tone for the entire 8.x line: lean on the type system, write less glue, and let small language features carry the weight that boilerplate used to. Almost everything that follows in 8.1 through 8.5 builds on the foundation laid here. ::note{label="PHP 8.x series"} This is part of a series on what's new in each modern PHP release. Next: [What's New in PHP 8.1](https://franktheprogrammer.com/articles/whats-new-in-php-8-1) :: # What's New in PHP 8.1 PHP 8.1 arrived on 25 November 2021, and it is the release that turned a lot of skeptics into fans. After 8.0 modernized the syntax, 8.1 filled in the features people had been faking for years: real enums, properties you cannot mutate, and a proper concurrency primitive. Here are the ones worth knowing. ## Enumerations Before 8.1, a fixed set of options meant class constants or bare strings, with nothing stopping an invalid value from sneaking through. Enums give you a real type whose instances are the only legal values, and they can carry methods. ```php enum OrderStatus: string { case Pending = 'pending'; case Paid = 'paid'; case Shipped = 'shipped'; case Canceled = 'canceled'; public function canCancel(): bool { return match ($this) { self::Pending, self::Paid => true, self::Shipped, self::Canceled => false, }; } } $status = OrderStatus::from('paid'); $status->canCancel(); // true ``` A backed enum maps each case to a scalar, so `OrderStatus::from('paid')` validates and returns the case, while `tryFrom('bogus')` returns null instead of throwing. This is the type-safe version of the constants idea I covered in [class constant visibility](https://franktheprogrammer.com/articles/setting-visibility-for-your-class-constants-in-php), with behavior attached. ## Readonly properties A property marked `readonly` can be written once, during initialization, and never again. It is the single cleanest way to build an immutable value object. ```php final class Coordinates { public function __construct( public readonly float $lat, public readonly float $lng, ) {} } $home = new Coordinates(40.7128, -74.0060); $home->lat = 41.0; // Error: Cannot modify readonly property ``` Combined with constructor promotion from 8.0, an immutable object is now a handful of lines, and any attempt to mutate it after construction fails loudly rather than corrupting state somewhere downstream. ## First-class callable syntax Passing a function around used to mean string names or the clunky `Closure::fromCallable()`. The new `...` syntax produces a real closure from any function or method, with full type information. ```php $lengths = array_map(strlen(...), ['php', 'is', 'fun']); // [3, 2, 3] $normalizer = $formatter->normalize(...); $clean = array_map($normalizer, $rawValues); ``` `strlen(...)` means "the strlen function as a closure," and `$formatter->normalize(...)` captures a bound method. It is concise, it is analyzable by your IDE and static analysis tools, and it works anywhere a callable is expected. ## Fibers Fibers are PHP 8.1's lightweight, cooperative concurrency primitive. A fiber can pause itself mid-execution and hand control back to the caller, then resume exactly where it left off. They are the engine behind modern async libraries, but the core idea is small enough to demo directly. ```php $fiber = new Fiber(function (): void { echo "fetching...\n"; $chunk = Fiber::suspend('need data'); echo "resumed with: $chunk\n"; }); $request = $fiber->start(); // prints "fetching..." then returns 'need data' $fiber->resume('here it is'); // prints "resumed with: here it is" ``` `Fiber::suspend()` freezes the function and returns its argument to whoever called `start()` or `resume()`. You hand a value back in, and execution picks up from the suspension point. You will rarely write this by hand, but understanding it demystifies how async PHP frameworks pull off non-blocking I/O. ## The never return type The `never` type documents that a function never returns normally: it always throws or exits. That tells both the engine and your tooling that any code after the call is unreachable. ```php function abort(int $status, string $message): never { http_response_code($status); echo $message; exit; } $user = find($id) ?? abort(404, 'Not found'); ``` Because the type system knows `abort()` cannot fall through, the analyzer is satisfied that `$user` is non-null afterward. It is a tiny annotation that makes guard clauses and redirect helpers type-check cleanly. ## New in initializers You can now use `new` to create objects in default parameter values, attribute arguments, and other initializer positions. This makes optional dependencies expressible without a null-and-assign dance in the constructor body. ```php final class ReportService { public function __construct( private Logger $logger = new NullLogger(), ) {} } $service = new ReportService(); // uses the NullLogger default ``` If a caller injects a real logger, it wins; otherwise the harmless default is used. The constructor body stays empty, and the default is right there in the signature where you can see it. PHP 8.1 is where the modern PHP toolkit really came together. Enums and readonly properties in particular changed how people model domains, and you will see both lean on the 8.0 features that came just before. Next up, 8.2 takes the immutability story further and tidies up the type system. ::note{label="PHP 8.x series"} This is part of a series on what's new in each modern PHP release. Previous: [What's New in PHP 8.0](https://franktheprogrammer.com/articles/whats-new-in-php-8-0) · Next: [What's New in PHP 8.2](https://franktheprogrammer.com/articles/whats-new-in-php-8-2) :: # What's New in PHP 8.2 PHP 8.2 shipped on 8 December 2022. It is a quieter release than 8.1, more about refinement than headline features, but the refinements are good ones: whole classes can be readonly, the type system gained new building blocks, and a couple of long-standing rough edges around traits and security finally got sanded down. Here is what stands out. ## Readonly classes PHP 8.1 gave us readonly properties. PHP 8.2 lets you mark an entire class readonly, which applies the modifier to every property at once. For a value object where nothing should ever change, this is the whole declaration. ```php readonly class Money { public function __construct( public int $amountCents, public string $currency = 'USD', ) {} } $price = new Money(1999, 'EUR'); $price->amountCents = 0; // Error: Cannot modify readonly property ``` No need to repeat `readonly` on each property the way the [8.1 example](https://franktheprogrammer.com/articles/whats-new-in-php-8-1) did. The class declaration says it once, and the engine enforces it everywhere, including for any dynamic properties (which are also forbidden, as we will see below). ## Disjunctive normal form types DNF types let you combine union (`|`) and intersection (`&`) types in a single declaration, using parentheses to group the intersections. This expresses constraints that neither alone could. ```php interface Identifiable { public function id(): int; } interface Timestamped { public function createdAt(): int; } function audit((Identifiable&Timestamped)|null $entity): void { if ($entity === null) { return; } log("#{$entity->id()} at {$entity->createdAt()}"); } ``` The signature reads as "either something that is both `Identifiable` and `Timestamped`, or null." Before 8.2 you could not mix the two operators, so a case like this meant giving up and falling back to a looser type. ## Standalone null, false, and true types `null`, `false`, and `true` became types you can use on their own. They are most useful as precise return types for functions that, by contract, only ever yield one of those values. ```php final class Cache { public function clear(): true { // ... always succeeds, or throws return true; } public function missingKey(): null { return null; } } ``` A function returning `: false` or `: true` tells the caller far more than `: bool` does, and pairs well with union types when a method returns either a real value or a sentinel `false`. ## The new Random extension PHP 8.2 introduced an object-oriented `Random` extension built around a `Randomizer` class and pluggable engines. It replaces the old global Mersenne Twister functions with something testable and explicit, and it bundles handy helpers. ```php $randomizer = new \Random\Randomizer(); $roll = $randomizer->getInt(1, 6); $shuffled = $randomizer->shuffleArray(['spades', 'hearts', 'clubs', 'diamonds']); $winner = $randomizer->pickArrayKeys($players, 1)[0]; ``` Because the engine is injectable, you can pass a seeded engine in a test and get deterministic "random" results, then swap in a cryptographically secure engine in production. No more global state quietly deciding your dice rolls. ## Constants in traits Traits can now declare constants. Previously only properties and methods were allowed, which forced awkward workarounds when a trait's behavior depended on a fixed value. Now the constant lives with the code that uses it. ```php trait HasHttpStatus { public const DEFAULT_STATUS = 200; public function statusOr(int $given): int { return $given ?: self::DEFAULT_STATUS; } } final class JsonResponse { use HasHttpStatus; } echo JsonResponse::DEFAULT_STATUS; // 200 ``` The constant is accessed through the using class, keeping the trait self-contained instead of leaning on the consumer to define the value. ## Sensitive parameter redaction Stack traces are great for debugging and terrible for secrets, because an exception thrown deep in a call stack can dump a password or API token straight into your logs. The `#[SensitiveParameter]` attribute tells PHP to redact that argument from traces. ```php function authenticate( string $username, #[SensitiveParameter] string $password, ): bool { throw new RuntimeException('auth service down'); } ``` When that exception bubbles up, the trace shows the username but replaces the password with `Object(SensitiveParameterValue)`. It is a one-line change that closes a genuinely common leak, with no impact on how the function behaves. PHP 8.2 is the kind of release that makes the previous year's features feel finished: readonly graduated from properties to whole classes, the type system filled in its gaps, and a couple of papercuts around traits and logging got fixed. Next, 8.3 keeps the momentum with typed constants and some sharp tooling additions. ::note{label="PHP 8.x series"} This is part of a series on what's new in each modern PHP release. Previous: [What's New in PHP 8.1](https://franktheprogrammer.com/articles/whats-new-in-php-8-1) · Next: [What's New in PHP 8.3](https://franktheprogrammer.com/articles/whats-new-in-php-8-3) :: # What's New in PHP 8.3 PHP 8.3 was released on 23 November 2023. There is no single blockbuster feature here, but it is a deeply practical release: it closes gaps in the type system, adds tooling that catches real mistakes, and ships a few standard-library functions you will wonder how you lived without. Here are the highlights. ## Typed class constants Class constants can finally carry a type declaration. Before 8.3, a child class could redefine a constant with a completely different type and PHP would shrug. Now the type is part of the contract and is enforced. ```php interface HasVersion { const string VERSION = '1.0'; } class App implements HasVersion { const string VERSION = '8.3'; const int MAX_UPLOAD = 10; } ``` If a subclass tried to override `VERSION` with an `int`, PHP would reject it at compile time. This finishes the work that promotions, readonly, and the standalone types from earlier releases started: putting real types on every member of a class. ## The Override attribute `#[Override]` tells PHP "this method is meant to replace a parent method," and the engine verifies that a matching parent method actually exists. It catches the classic bug where a typo silently creates a brand-new method instead of overriding the one you intended. ```php class TestCase { protected function tearDown(): void {} } class UserTest extends TestCase { #[Override] protected function taerDown(): void // Fatal error: nothing to override { // cleanup that would never have run without the attribute } } ``` Without the attribute, that misspelled `taerDown()` would just sit there, never called, and your cleanup would quietly never run. With it, PHP refuses to compile until the name matches. ## json\_validate() Checking whether a string is valid JSON used to mean calling `json_decode()` and inspecting the error, which wastes memory building a structure you are about to throw away. `json_validate()` answers the yes-or-no question without allocating the result. ```php $payload = file_get_contents('php://input'); if (!json_validate($payload)) { http_response_code(400); exit('Invalid JSON'); } $data = json_decode($payload, true); ``` For large payloads, or a hot path that rejects bad input early, this is meaningfully cheaper than decoding twice. It is a small function with a very clear job. ## Dynamic class constant fetch You can now fetch a constant whose name is held in a variable using `Class::{$name}` syntax, the same way dynamic property and method access already worked. ```php enum Suit: string { case Hearts = 'H'; } class Settings { const string THEME_DARK = 'midnight'; const string THEME_LIGHT = 'daylight'; } $choice = 'THEME_DARK'; echo Settings::{$choice}; // 'midnight' ``` Before 8.3 this meant reaching for the `constant()` function with a clumsy string of the fully qualified name. The new syntax is both shorter and consistent with the rest of the language. ## Random string generation The `Random` extension from [PHP 8.2](https://franktheprogrammer.com/articles/whats-new-in-php-8-2) gained `getBytesFromString()`, which builds a random string from exactly the alphabet you give it. That makes generating tokens, codes, and slugs a one-liner. ```php $randomizer = new \Random\Randomizer(); $alphabet = '0123456789abcdefghijklmnopqrstuvwxyz'; $token = $randomizer->getBytesFromString($alphabet, 12); // e.g. "q3v9zk1mb7xa" ``` You control the character set, so there are no surprise symbols to URL-encode or confusing look-alike characters if you do not want them. PHP 8.3 also added `getFloat()` for unbiased random floats within an interval, rounding out the extension nicely. ## Readonly deep cloning Readonly properties are write-once, which created a real problem for `__clone()`: you could not give a cloned object fresh copies of its nested objects, because reassigning the property was forbidden. PHP 8.3 allows readonly properties to be written exactly once inside `__clone()`. ```php final class Order { public function __construct( public readonly Address $shipTo, ) {} public function __clone(): void { $this->shipTo = clone $this->shipTo; // now allowed in __clone } } ``` Cloning an `Order` now produces an independent `Address` rather than two orders quietly sharing one. This made readonly value objects practical to copy, which sets the stage for the much nicer cloning syntax that arrives in 8.5. PHP 8.3 is the kind of release you upgrade to without drama and then quietly enjoy. The typed constants and `#[Override]` attribute prevent bugs, and the new functions remove little chores. Next, 8.4 brings one of the biggest syntax additions of the whole 8.x line: property hooks. ::note{label="PHP 8.x series"} This is part of a series on what's new in each modern PHP release. Previous: [What's New in PHP 8.2](https://franktheprogrammer.com/articles/whats-new-in-php-8-2) · Next: [What's New in PHP 8.4](https://franktheprogrammer.com/articles/whats-new-in-php-8-4) :: # What's New in PHP 8.4 PHP 8.4 landed on 21 November 2024, and it carries the most significant syntax change since enums: property hooks. Alongside it came asymmetric visibility, a cleaner way to chain off `new`, some array functions people had been requesting for a decade, and a brand-new HTML5 DOM parser. This is a big one. ## Property hooks Property hooks let a property compute its value on read or transform it on write, without you writing a separate getter and setter method. The property stays a property from the caller's point of view, but you control what happens behind it. ```php final class Temperature { public function __construct(public float $celsius = 0.0) {} public float $fahrenheit { get => $this->celsius * 9 / 5 + 32; set (float $value) => $this->celsius = ($value - 32) * 5 / 9; } } $t = new Temperature(25); echo $t->fahrenheit; // 77 $t->fahrenheit = 212; echo $t->celsius; // 100 ``` `$fahrenheit` stores nothing of its own. Reading it derives a value from `celsius`, and writing it updates `celsius` in turn. Callers just use `$t->fahrenheit` like any other property, with none of the getter/setter ceremony, and the hook is a natural home for validation or normalization too. ## Asymmetric visibility Sometimes you want a property the whole world can read but only the class itself can change. Before 8.4 that meant a private property plus a public getter. Now you can say it in one declaration. ```php final class ShoppingCart { public private(set) array $items = []; public function add(Product $product): void { $this->items[] = $product; } } $cart = new ShoppingCart(); echo count($cart->items); // reading is fine $cart->items = []; // Error: cannot modify private(set) property ``` `public private(set)` means public to read, private to write. Outside code can inspect the cart's items but cannot reach in and replace them, so the only way to change the contents is through `add()`. I used this exact pattern in [SOLID Principles in Modern PHP](https://franktheprogrammer.com/articles/solid-principles-modern-php) to model a registry. ## new without parentheses A small but delightful change: you can now call a method or access a property directly on a freshly constructed object without wrapping the `new` expression in parentheses. ```php // Before 8.4 $name = (new ReflectionClass($model))->getShortName(); // PHP 8.4 $name = new ReflectionClass($model)->getShortName(); ``` The extra parentheses were pure noise, and now they are gone. It reads exactly the way you would say it: make one of these, then call this on it. ## The array\_find family PHP finally shipped the higher-order array functions that every other language has had forever. `array_find` returns the first matching element, while `array_any` and `array_all` answer boolean questions, all without writing a loop and a flag variable. ```php $users = $repository->all(); $firstAdmin = array_find($users, fn (User $u) => $u->role === 'admin'); $hasAdmin = array_any($users, fn (User $u) => $u->role === 'admin'); $allActive = array_all($users, fn (User $u) => $u->isActive); ``` `array_find` returns the matching user or null, so it pairs naturally with the [nullsafe operator](https://franktheprogrammer.com/articles/whats-new-in-php-8-0). There is also `array_find_key` when you want the key instead of the value. Together they replace a surprising amount of boilerplate loop code. ## The Deprecated attribute You can now mark your own functions, methods, and constants as deprecated with `#[Deprecated]`, and PHP will emit a deprecation notice whenever they are used. Until 8.4 this was only possible for internal PHP functions. ```php final class PaymentGateway { #[Deprecated(message: 'use charge() instead', since: '2.5.0')] public function makePayment(int $cents): void { $this->charge($cents); } public function charge(int $cents): void { /* ... */ } } ``` Anyone calling `makePayment()` gets a deprecation warning pointing them at `charge()`, and static analysis tools surface it too. It is a clean way to guide consumers of your library off an old API without breaking them overnight. ## A modern HTML5 DOM The old DOM extension predated HTML5 and mangled modern markup. PHP 8.4 added `Dom\HTMLDocument`, a spec-compliant parser with the `querySelector` API you already know from the browser. ```php $dom = Dom\HTMLDocument::createFromString( '

Hello

World

' ); $title = $dom->querySelector('.title'); echo $title->textContent; // "Hello" ``` No more wrestling with `DOMDocument::loadHTML()` and its libxml warnings on perfectly valid HTML5. Scraping, transforming, or testing markup is now genuinely pleasant, with CSS selectors instead of XPath gymnastics. PHP 8.4 is a landmark release. Property hooks and asymmetric visibility change how you design classes, and the smaller additions sand down chores you have lived with for years. The final stop in this series, 8.5, leans into composition with the pipe operator and a much nicer way to clone objects. ::note{label="PHP 8.x series"} This is part of a series on what's new in each modern PHP release. Previous: [What's New in PHP 8.3](https://franktheprogrammer.com/articles/whats-new-in-php-8-3) · Next: [What's New in PHP 8.5](https://franktheprogrammer.com/articles/whats-new-in-php-8-5) :: # What's New in PHP 8.5 PHP 8.5 was released on 20 November 2025, closing out a remarkable run of yearly releases. Where 8.4 was about how you design classes, 8.5 is about how you compose and use them: a pipe operator for chaining, a clean way to clone-with-changes, and a built-in URI parser, among others. Here is the tour, and the end of our series. ## The pipe operator The pipe operator `|>` takes the value on its left and feeds it as the argument to the callable on its right, so you can read a transformation as a top-to-bottom pipeline instead of an inside-out nest of function calls. ```php $slug = ' Hello, Fellow PHP Devs! ' |> trim(...) |> strtolower(...) |> (fn (string $s) => preg_replace('/[^a-z0-9]+/', '-', $s)) |> (fn (string $s) => trim($s, '-')); echo $slug; // "hello-fellow-php-devs" ``` Each step gets the previous step's output. Compare that to `trim(preg_replace(..., strtolower(trim($input))))`, which you have to read from the inside out. The right-hand side is any callable, so the [first-class callable syntax](https://franktheprogrammer.com/articles/whats-new-in-php-8-1) from 8.1 and short closures slot right in. ## Cloning with property updates PHP 8.3 made it possible to deep-clone readonly objects inside `__clone()`. PHP 8.5 goes further: `clone()` now takes an array of property values to overwrite on the copy, which makes the immutable "with-er" pattern a single expression. ```php final class Color { public function __construct( public readonly int $r, public readonly int $g, public readonly int $b, public readonly int $alpha = 255, ) {} public function withAlpha(int $alpha): static { return clone($this, ['alpha' => $alpha]); } } $blue = new Color(0, 0, 255); $ghost = $blue->withAlpha(64); // a new Color, original untouched ``` The original object is never mutated; you get a fresh copy with just the named properties changed. This is exactly the immutable update that used to require a full constructor call repeating every unchanged field. ## The NoDiscard attribute Some return values are too important to ignore: the boolean that says whether a write succeeded, the new immutable object from a with-er. `#[NoDiscard]` makes PHP warn when a call's return value is thrown away. ```php final class Config { #[NoDiscard('the returned Config is the updated one; this is immutable')] public function withDebug(bool $on): static { return clone($this, ['debug' => $on]); } } $config->withDebug(true); // Warning: return value not used $config = $config->withDebug(true); // correct (void) $config->withDebug(true); // explicitly discard, no warning ``` It catches the very common mistake of calling an immutable method and forgetting to capture its result, which otherwise does nothing at all. If you really mean to discard the value, casting to `(void)` says so and silences the warning. ## array\_first and array\_last PHP 7.3 gave us `array_key_first()` and `array_key_last()`. PHP 8.5 completes the set with `array_first()` and `array_last()`, which return the first and last values directly. ```php $events = $log->recent(); $latest = array_last($events); // last value, or null if empty $oldest = array_first($events); // first value, or null if empty ``` No more `$arr[array_key_last($arr)]` or `end($arr)` with its internal-pointer side effects. They return null on an empty array, so they pair naturally with the null coalescing operator and the [array\_find family](https://franktheprogrammer.com/articles/whats-new-in-php-8-4) from 8.4. ## The URI extension Parsing URLs in PHP has long meant `parse_url()`, which is loose about edge cases and not standards-compliant. PHP 8.5 ships a proper URI extension with classes that follow RFC 3986 and the WHATWG URL standard. ```php use Uri\Rfc3986\Uri; $uri = new Uri('https://example.com:8080/blog/post?ref=newsletter#top'); $uri->getScheme(); // "https" $uri->getHost(); // "example.com" $uri->getPort(); // 8080 $uri->getPath(); // "/blog/post" $uri->getQuery(); // "ref=newsletter" ``` You get real parsing, normalization, and validation instead of a best-effort associative array. For anything that handles user-supplied links, that correctness matters: it is the difference between rejecting a malformed URL and quietly mishandling it. ## Backtraces on fatal errors A long-standing debugging annoyance: fatal errors printed a message but no stack trace, so you knew what broke but not how you got there. PHP 8.5 includes a backtrace with fatal errors by default. ```ini ; enabled by default in PHP 8.5 fatal_error_backtraces = On ``` Now a fatal error shows the call chain that led to it, the same way an uncaught exception always has. It is a quiet change, but the first time a production fatal hands you a full trace instead of a single cryptic line, you will appreciate it. That wraps our walk from PHP 8.0 to 8.5. Step back and the arc is clear: 8.0 modernized the syntax, 8.1 and 8.2 made immutability and types first-class, 8.3 and 8.4 reshaped how you design classes, and 8.5 makes composing them a pleasure. If you started this series on the [8.0 article](https://franktheprogrammer.com/articles/whats-new-in-php-8-0), the contrast between then and now is its own little lesson in how far the language has come. ::note{label="PHP 8.x series"} This is the final part of a series on what's new in each modern PHP release. Previous: [What's New in PHP 8.4](https://franktheprogrammer.com/articles/whats-new-in-php-8-4) :: # Yarn: Fast and Secure Dependency Management Yarn is a super simple dependency management tool which acts as a drop-in replacement for npm, so you can get started using it right away. The best way to install yarn is by using npm. That's right, you use npm to install yarn, npm's replacement essentially, how fun is that! ::note{label="From 2026"} When this was written, Yarn was notably faster and more secure than npm. Since then npm has caught up: it uses a lockfile, caches packages, and verifies integrity by default, so the "faster and more secure" framing is dated. Both are solid choices today, and Yarn now offers a modern Berry line (Yarn 2+ with Plug'n'Play). Pick whichever your team prefers. :: There are a few great reasons as to why you might start using yarn today. - Yarn sets up a lock file with the exact versions being used within a project. This improves the speed of installation dramatically and also allows you to share your lock file with other developers to ensure that everyone involved is using the same version numbers of a given package. - Yarn also caches every package it downloads locally, this way you never need to download the same package twice. - Yarn also installs packages in parallel improving the speed in which projects are set up. - Yarn also uses checksums to install packages, making it super secure and validating the packages before any code is run within your codebase. ## Installation All of the following installation instructions are available on the yarn documentation. I have included the information below for ease of reference. ### via npm My preferred method, if you have npm already installed on your system, is to simply run the following command to get yarn easily installed. ```bash npm install --global yarn ``` There are plenty of other options to get it installed. I have included the link below to the yarn documentation for you to pick the option that works best for you. [Yarn Installation Documentation](https://yarnpkg.com/en/docs/install){rel=""nofollow""} ## Usage within a project Now that we have everything installed, we need to start configuring our project to use yarn. Once within your project root directory, you'll want to run the following command. ```bash yarn init ``` If you already have a yarn.lock file within your project, or if you have a package.json file you can run the following command. ```bash yarn install ``` Or you could simply just type ```bash yarn ``` ### Adding new packages/modules Installing packages is super simple now that you have yarn configured. The quickest and easiest way is by using the following. ```bash yarn add [package] ``` If you want to add a specific version to your project instead of using the latest version of a package you'd run the following. ```bash yarn add [package]@[version] ``` You can also specify a specific tagged version of a package by running the following command. ```bash yarn add [package]@[tag] ``` ### Deleting a Package Deleting/Removing a package could not be any easier. Simply run the following command and yarn will take care of the rest for you. ```bash yarn remove [package] ``` ### Updating/Upgrading a Package To upgrade an existing package to the latest version you'd just need to run the following command. ```bash yarn upgrade [package] ``` You can also specify what version to update to by running the following command. ```bash yarn upgrade [package]@[version] ``` If you want to upgrade a specific tagged version, then you could run the following as well. ```bash yarn upgrade [package]@[tag] ``` # Yarn: Publishing a Package Publishing a package to the npm repository has never been simpler. With a few steps you can create a package that is redistributable to all of your projects. If you do not have yarn already configured, [click here for setup instructions](https://franktheprogrammer.com/articles/yarn-fast-and-secure-dependency-management). One thing to note when publishing a package. Once you publish a package at a specific version, you can never change that version again. ## Publishing a Package Within your projects root directory you'll have a package.json file which will define all of the details regarding your package. Within that directory you'd run the following command to publish your package. ```bash yarn publish ``` You'll be prompted for your username and password for [npmjs.com](http://npmjs.com/){rel=""nofollow""}. If you do not already have an account, be sure to create your account by [Clicking Here](https://www.npmjs.com/signup){rel=""nofollow""}. If you have a gzipped tarball .tgz of your package that you want to publish instead you can run the following command. ```bash yarn publish [tarball] ``` If you're outside of your projects root directory, you can define the folder you wish to publish as so. ```bash yarn publish [folder] ``` You'll want to make sure that the folder you're publishing contains a package.json file within the root of the directory. If you want to publish a specific tag of your package, then use the following command within the project's root directory to specify the tag. ```bash yarn publish --tag ``` You can also control the access within the npm registry to a package you're publishing by defining the access flag as so. ```bash yarn publish --access ``` ## Unpublish A Package From The Repository You'll want to be careful with this command, as if anyone is using your package you can cause issues for them. To remove a package from the [npmjs.com](http://npmjs.com/){rel=""nofollow""} repository simply run the following. ```bash npm unpublish --force ``` At the time of this writing, there does not seem to be a way that I'm familiar with to unpublish a package via yarn so the traditional npm method will suffice. Before you are able to use this command you will also need to log in to [npmjs.com](http://npmjs.com/){rel=""nofollow""} via the command line by running npm login within your project's root directory. ## Example Package First we'll start by creating a blank directory named logger. Within the directory, let's start by running the following command which will create our package.json file. ```bash yarn init ``` This command will take you through a series of questions and then output a file package.json with the following contents. ```json { "name": "example-logger", "version": "1.0.0", "description": "Logs a message to the console.", "main": "index.js", "author": "Frank Perez ", "license": "MIT" } ``` Your file will contain slightly different information, depending on how you answered each of the prompts previously. Keep in mind that the name should also be unique to what you find in [npmjs.com](http://npmjs.com/){rel=""nofollow""}, if not your package will fail when attempting to publish. We now have our package.json file and listed that our main file would be named index.js. Let's now create the file and put in the code we want for the package. Here are the contents for my index.js file. ```javascript exports.logger = function() { console.log('Hello from our Logger Package.'); } ``` Once you have your files setup, you can now run the command yarn publish within your projects root directory. Shortly after it finishes, you will get a confirmation message letting you know that your package was published successfully. Now to install your newly created package you'd simply run the following command within your future projects. ```bash yarn add example-logger ```