# Kirschbaum Development Group
> Kirschbaum Development Group is a team of dozens of senior Laravel developers providing expert consulting, custom software development, short- or long-term contract developers, team augmentation, and embedded teams. We specialize in building secure, scalable web applications, custom AI/LLM integrations, SaaS products, and DevOps solutions using Laravel, React, Vue.js, and PHP for industries including healthcare, financial services, legal, life sciences, energy, and commerce. Our case studies demonstrate a proven track record across complex projects, and our insights articles feature in-depth technical expertise from our engineering team.
## Pages
- [Homepage](https://kirschbaumdevelopment.com): Kirschbaum provides expert custom software development, Laravel consulting, and team augmentation to help businesses build and scale secure web applications.
- [Contact](https://kirschbaumdevelopment.com/contact): Get in touch with Kirschbaum Development to discuss your custom software needs, request a project consultation, or learn more about our services.
- [About](https://kirschbaumdevelopment.com/about): Learn about Kirschbaum Development Group, our mission, our remote-first culture, and the passionate team of experts building world-class software.
- [Solutions](https://kirschbaumdevelopment.com/solutions): Explore our specialized software solutions, designed to solve complex business challenges with scalable architecture, reliable infrastructure, and clean code.
- [AI Solutions](https://kirschbaumdevelopment.com/solutions/ai): Leverage artificial intelligence in your business. Kirschbaum builds custom AI solutions, LLM integrations, and automated workflows to drive innovation.
- [Laravel DevOps](https://kirschbaumdevelopment.com/solutions/devops): Ensure your applications are scalable and secure. We offer DevOps consulting, AWS infrastructure design, and automated deployment pipelines.
- [SAAS Products](https://kirschbaumdevelopment.com/solutions/saas): Build a robust Software as a Service platform. We specialize in custom SaaS development, multi-tenant architecture, and recurring billing integration.
- [Mobile Solutions](https://kirschbaumdevelopment.com/solutions/mobile): Bring your vision to iOS and Android. Kirschbaum builds high-performance, cross-platform mobile applications using React Native and Expo.
- [Team](https://kirschbaumdevelopment.com/team): Meet the talented engineers, architects, and designers at Kirschbaum Development who are dedicated to delivering exceptional custom software.
- [Careers](https://kirschbaumdevelopment.com/careers): Join our remote-first team of passionate developers. View current job openings at Kirschbaum Development and apply to work on exciting software projects.
- [Privacy Policy](https://kirschbaumdevelopment.com/privacy-policy): Read the Kirschbaum Development Privacy Policy to understand how we collect, use, and protect your personal data when you use our website.
- [Terms of Service](https://kirschbaumdevelopment.com/terms-of-service): Review the Terms of Service for Kirschbaum Development. These terms govern your use of our website and outline our legal obligations and user rights.
- [Team Augmentation](https://kirschbaumdevelopment.com/services/team-augmentation): Expand your team with Kirschbaum’s Laravel-focused augmentation services. Add senior Laravel, Next.js, DevOps, and AI engineers who integrate seamlessly and deliver fast.
- [Technical Leadership](https://kirschbaumdevelopment.com/services/technical-leadership): Fractional CTO support with architecture, AI strategy, and 10+ years of Laravel expertise. Clear technical direction tailored to your team’s goals and systems.
- [Rescue Projects](https://kirschbaumdevelopment.com/services/rescue-projects): Laravel experts who rescue stalled software projects, stabilize systems, and restore delivery momentum fast.
- [Technology](https://kirschbaumdevelopment.com/technology): Discover the technologies we use to build scalable software, including Laravel, Filament, Next.js, React Native, Vue, and modern cloud infrastructure.
- [Laravel Technology](https://kirschbaumdevelopment.com/technology/laravel): Kirschbaum designs scalable, secure Laravel applications with clean architecture, modern tooling, and direct developer collaboration. Build what lasts - and evolves.
- [Filament Technology](https://kirschbaumdevelopment.com/technology/filament): Accelerate your admin panel development. Kirschbaum utilizes Filament PHP to rapidly build beautiful, functional TALL-stack administrative interfaces.
- [NextJS technology](https://kirschbaumdevelopment.com/technology/nextjs): Build lightning-fast, SEO-optimized web applications with Next.js. Kirschbaum delivers high-performance React frontends tailored to your business needs.
- [Vue Technology](https://kirschbaumdevelopment.com/technology/vue): Create dynamic and interactive user interfaces with Vue.js. Kirschbaum builds modern, responsive frontend applications that enhance user experience.
- [Web Development Services](https://kirschbaumdevelopment.com/services): From project-based delivery to ongoing support, Kirschbaum offers comprehensive custom software development services tailored to your business goals.
- [Case Studies](https://kirschbaumdevelopment.com/cases): Read our case studies to see how Kirschbaum Development has solved complex technical challenges and delivered successful custom software solutions.
- [Legal Industry](https://kirschbaumdevelopment.com/sector/legal-industry): Custom software solutions for the legal industry. We build secure case management systems, document automation tools, and client portals for law firms.
- [Healthcare](https://kirschbaumdevelopment.com/sector/healthcare): HIPAA-compliant custom software for the healthcare sector. We develop patient portals, telemedicine apps, and secure data management systems.
- [Commerce and Logistics](https://kirschbaumdevelopment.com/sector/commerce-and-logistics): Streamline operations with custom software for commerce and logistics. We build inventory management, supply chain tracking, and custom eCommerce platforms.
- [Life Sciences and Pharma](https://kirschbaumdevelopment.com/sector/life-sciences-and-pharma): Secure and compliant software solutions for life sciences and pharmaceuticals. We develop robust applications for research, trials, and data management.
- [Financial Services](https://kirschbaumdevelopment.com/sector/financial-services): Custom software solutions for financial services. We build secure trading platforms, financial management tools, and FinTech applications.
- [Energy Sector](https://kirschbaumdevelopment.com/sector/energy): Streamline energy operations with custom software. We build specialized tools for data tracking, resource management, and energy sector logistics.
- [Support & Enablement](https://kirschbaumdevelopment.com/services/support): Ensure your software runs smoothly long after launch. Kirschbaum provides reliable ongoing support, bug fixes, and feature enhancements.
- [Insights](https://kirschbaumdevelopment.com/insights): Explore software development insights from the engineers at Kirschbaum. Read expert articles on, AI, React, Laravel, React Native, DevOps, architecture, performance, and modern application development.
- [React](https://kirschbaumdevelopment.com/technology/react): Partner with expert React developers to build scalable, high-performance web applications. Kirschbaum delivers modern React solutions using TypeScript, Next.js, Inertia, and Laravel for startups and enterprise teams.
- [Engineering what's next](https://kirschbaumdevelopment.com/services/engineering-whats-next): Outgrown your current web app? We help teams assess, stabilize, and scale software from prototype to production without a full rewrite. Talk to our engineers!
## Case Studies
- [Securing and encrypting a custom-built CRM](https://kirschbaumdevelopment.com/cases/securing-and-encrypting-a-custom-built-crm): Read how Kirschbaum developed a highly secure, custom-built CRM featuring robust data encryption protocols for maximum data protection.
- [Turning SaaS features into competitive advantage](https://kirschbaumdevelopment.com/cases/turning-saas-features-into-competitive-advantage): Discover how Kirschbaum helped a SaaS company gain a competitive edge by strategically building and integrating innovative new features.
- [Modernizing an ecommerce subscription platform](https://kirschbaumdevelopment.com/cases/modernizing-an-ecommerce-subscription-platform): See how Kirschbaum successfully modernized a legacy ecommerce subscription platform, improving performance, scale, and user experience.
- [Building an investment and trading platform](https://kirschbaumdevelopment.com/cases/building-an-investment-and-trading-platform): Learn how Kirschbaum engineered a secure, high-performance custom investment and trading platform from the ground up.
- [Transforming subscription platforms without interruption](https://kirschbaumdevelopment.com/cases/transforming-subscription-platforms-without-interruption): Read how our team completely transformed a major subscription platform with zero downtime or interruption to existing active users.
- [Modernizing in-person lead generation technologies](https://kirschbaumdevelopment.com/cases/modernizing-in-person-lead-generation-technologies): Discover how Kirschbaum updated in-person lead generation technologies, replacing outdated systems with modern, efficient digital solutions.
- [Standardizing ecommerce infrastructure at scale](https://kirschbaumdevelopment.com/cases/standardizing-ecommerce-infrastructure-at-scale): See how Kirschbaum standardized and scaled the underlying ecommerce infrastructure for a growing enterprise business.
- [Orchestrating real estate settlements across incompatible systems](https://kirschbaumdevelopment.com/cases/orchestrating-real-estate-settlements-across-incompatible-systems): Learn how we successfully orchestrated complex real estate settlement workflows across multiple previously incompatible software systems.
- [Enhancing education with AI-powered chat](https://kirschbaumdevelopment.com/cases/enhancing-education-with-ai-powered-chat): Read how Kirschbaum integrated an AI-powered chat solution into an educational platform to enhance student engagement and learning outcomes.
- [Revolutionizing a lead-generation platform with AI](https://kirschbaumdevelopment.com/cases/revolutionizing-a-lead-generation-platform-with-ai): Discover how we revolutionized a traditional lead generation platform by integrating custom artificial intelligence and automation tools.
- [Transforming aromatherapy education and retail](https://kirschbaumdevelopment.com/cases/transforming-aromatherapy-education-and-retail): See how Kirschbaum transformed digital operations for an aromatherapy business, unifying their educational platform and online retail store.
- [Revitalizing a music production marketplace](https://kirschbaumdevelopment.com/cases/revitalizing-a-music-production-marketplace): Learn how our team revitalized a struggling music production marketplace by optimizing the codebase and improving the user experience.
- [Enabling global marketing for a pharmaceutical company](https://kirschbaumdevelopment.com/cases/enabling-global-marketing-for-a-pharmaceutical-giant): Read how Kirschbaum engineered a compliant software solution to enable scalable, global marketing operations for a major pharmaceutical company.
- [Enterprise-grade authentication for a healthcare platform](https://kirschbaumdevelopment.com/cases/enterprise-grade-authentication-for-a-healthcare-platform): Discover how we implemented secure, enterprise-grade authentication and access controls for a sensitive healthcare software platform.
- [Rebuilding provider configuration for a healthcare staffing platform](https://kirschbaumdevelopment.com/cases/provider-configuration-for-a-healthcare-staffing-platform): See how Kirschbaum developed a complex provider configuration tool to streamline operations for a healthcare staffing platform.
- [Rethinking a socially-driven restaurant discovery mobile app](https://kirschbaumdevelopment.com/cases/socially-driven-restaurant-discovery-mobile-app): Discover how Kirschbaum Development built a React Native restaurant discovery app that combines social recommendations, location intelligence, interactive maps, and real-time data aggregation to create a more personal dining experience across iOS and Android.
- [Building a cultural geospatial mobile app](https://kirschbaumdevelopment.com/cases/building-cultural-geospatial-mobile-app): See how Kirschbaum Development built a React Native mobile application that combines geolocation, interactive maps, educational content, and event discovery to create an engaging, location-aware cultural exploration experience across iOS and Android.
## Insights
- [Custom Laravel package development](https://kirschbaumdevelopment.com/insights/custom-laravel-packages): If you don't already have a Laravel project up and running from which you would like to develop your package, go ahead and create a fresh install of Laravel so you can build the package there.
- [Laravel Translations Loader](https://kirschbaumdevelopment.com/insights/laravel-translations-loader): Laravel Translations Loader is a webpack loader that enables you to load your Laravel translations into your javascript bundle.
- [Nova Inline Select](https://kirschbaumdevelopment.com/insights/nova-inline-select): Laravel Nova is a fantastic tool that we at Kirschbaum Development have been using for developing both client projects and internal ones.
- [Laravel Github Actions](https://kirschbaumdevelopment.com/insights/laravel-github-actions): Github Actions is a very powerful way to automate things. In this post, we will look into how to configure
your Laravel application to use Github Actions.
- [Mail intercept](https://kirschbaumdevelopment.com/insights/mail-intercept): Mail Intercept for Laravel is a new way of testing mail by intercepting, not faking, email so we can dissect it, turn it upside down, and inspect everything. Under the hood, it is quite simple in that it forces the mail driver to be an array pushing all those emails into memory. We then grab those emails and run assertions on them!
- [Eloquent power joins with Laravel](https://kirschbaumdevelopment.com/insights/power-joins): If you have some experience using databases, it is very likely you have used `joins` at least once in your career. Joins can be used for many reasons, from selecting data from other tables to limiting the matches of your query.
- [How Tailwind CSS adds value in web development](https://kirschbaumdevelopment.com/insights/how-tailwind-css-adds-value-in-web-development): Explore how Tailwind CSS accelerates web development with utility-first styling and a flexible design system.
- [Why we love Laravel](https://kirschbaumdevelopment.com/insights/why-we-love-laravel): Leveraging a robust framework like Laravel enables us to provide our clients with business solutions at a pace that wasn’t possible before. We’ve been fans since we started working with it in 2013 and, having seen the value it brings to their business, our clients are fans as well. Here are the things we love about it as developers and why you’ll love it too.
- [Leveraging language in dev culture](https://kirschbaumdevelopment.com/insights/leveraging-language-in-dev-culture): Learn how language and communication shape development culture and improve team sprints and iterations.
- [Structuring and testing your Laravel Events and Listeners](https://kirschbaumdevelopment.com/insights/structuring-and-testing-your-laravel-events-and-listeners): In this article, we explore one simple yet powerful and scalable approach to setting up and testing your events and listeners.
- [Laravel OpenAPI Validator](https://kirschbaumdevelopment.com/insights/laravel-openapi-validator): Chances are pretty good you're exposing an API specification, probably through documentation and/or something like an OpenAPI spec (if you're not, please please please drop everything you're doing and take care of that). This is a spectacular way to create a clear line of communication to your users on how you'd like them to interact. Even better yet, if you hold steadfast to your "word" (spec), you'll have a steady stream of followers pining to use your app!
- [Implement a custom driver for Laravel Socialite](https://kirschbaumdevelopment.com/insights/implement-a-custom-driver-for-laravel-socialite): Laravel Socialite is an official Laravel package to authenticate with OAuth providers. It supports authentication with Facebook, Twitter, LinkedIn, Google, GitHub, and Bitbucket. But, what if you want to use a different driver?
- [Import Laravel Vapor DNS to Cloudflare](https://kirschbaumdevelopment.com/insights/introducing-the-orrison-cumulus-open-source-tool): Trying to manage DNS information from Vapor to Cloudflare without Orrison/Cumulus can open your data up to risks such as human error and wasted time since it would need to be copied over manually. In its essence, Orrison/Cumulus is an open-source tool that automatically copies the proper DNS records from Laravel Vapor to Cloudflare.
- [Leveraging virtual generated columns](https://kirschbaumdevelopment.com/insights/leveraging-virtual-generated-columns): When MySQL released native JSON column types in 5.7.8, it provided developers with an easier way to store and retrieve data for applications that used it as the storage engine. Laravel quickly supported it all the way back in 5.3 in data migrations and querying with Eloquent.
- [Code that can handle failure](https://kirschbaumdevelopment.com/insights/code-that-can-handle-failure): If there’s one thing that’s certain, it’s that your code will fail. Failed code is not always your fault. There’s a number of factors outside of your control that can cause failure, things like a network blip, infrastructure, outages, unhandled exceptions, cascading failures, and so on. Writing code that can deal with these situations helps you avoid common incidents, unnecessary alerts, and noisy logs. And hopefully, give you a better night of sleep.
- [Extending PHP enums with attributes](https://kirschbaumdevelopment.com/insights/extending-php-enums-with-attributes): Borrowed from the concept of annotations in other languages, PHP attributes can add powerful functionality to your enums.
- [How to build sequences with Laravel pipelines](https://kirschbaumdevelopment.com/insights/build-sequences-with-laravel-pipelines): Pipelines are a design pattern that enables the creation and execution of a sequence of operations. Much like an assembly line, where each step prepares a product for the next station along the line, pipelines are a group of functions linked together, so the output of the preceding function serves as the input of the following function. Laravel employs this pattern internally, such as in middleware.
- [Improving your password security](https://kirschbaumdevelopment.com/insights/improving-your-password-security): Managing passwords across our digital lives is a tedious but necessary task. It can be a daunting one for a business with no clear cybersecurity policy in place. A robust policy limits the vectors of attack for your business and your clients and ensures you can take swift action to remedy the issue if there’s a breach. Here are a few recommendations we have for better password security.
- [How to validate command parameters in Laravel](https://kirschbaumdevelopment.com/insights/validating-command-parameters-in-laravel): As Laravel developers, we create many complex commands for our applications. One question that always arises when creating commands is how to validate input parameters. Laravel commands offer a lot of flexibility when it comes to argument and option inputs. However, ensuring that the user is passing the right parameters is an important step in creating a solid command.
- [I’m adding a second server to my app. What now?](https://kirschbaumdevelopment.com/insights/adding-a-second-server-to-your-app): Adding a second server to your app can be a great way to improve your app's performance and/or increase its reliability. However, there are a couple of things you need to keep in mind when adding a second server.
- [Kirschbaum partners with Filament](https://kirschbaumdevelopment.com/insights/kirschbaum-filament): Kirschbaum is the official development agency of Filament!
- [Strategic advantages of dynamic software](https://kirschbaumdevelopment.com/insights/strategic-advantages-of-dynamic-software): Your tailored software is built to solve your unique business obstacles and can directly address any number of company-specific or industry-wide challenges.
- [How to build a CSV export system with Laravel](https://kirschbaumdevelopment.com/insights/how-to-build-a-csv-export-system-with-laravel): Craft robust CSV export functionality in your Laravel application. Leverage queues to handle large datasets efficiently, ensuring security and optimal performance.
- [How we approach DevOps at Kirschbaum](https://kirschbaumdevelopment.com/insights/how-we-approach-devops-at-kirschbaum): At Kirschbaum, our approach to DevOps for Laravel development is designed to meet the unique needs of each client, focusing on established processes, collaboration, automation, and innovation.
- [Why we love Vue.js](https://kirschbaumdevelopment.com/insights/why-we-love-vuejs): Vue can certainly stand on its own as a formidable frontend technology, but it can also integrate seamlessly and flexibly with Laravel, engendering a comprehensive solution to building dynamic, interactive, and modern full-stack web applications.
- [Why we love Filament](https://kirschbaumdevelopment.com/insights/why-we-love-filament): Filament is a full-stack UI framework for Laravel that accelerates development by providing features and functionality that usually requires multiple packages.
- [Tailwind CSS vs Bootstrap: Which is Better for Your Project?](https://kirschbaumdevelopment.com/insights/tailwind-vs-bootstrap): Discover the differences between Tailwind CSS and Bootstrap. Learn which CSS framework suits your web development needs best. Read on for a detailed comparison.
- [Optimizing JSON columns in Laravel](https://kirschbaumdevelopment.com/insights/optimizing-json-columns-in-laravel): Unlike standard JSON columns, virtual columns are calculated automatically from existing data but can be indexed, making them faster for querying. This improves sorting and filtering performance significantly, especially in large datasets where execution time is critical.
- [Tailwind Transition Colors](https://kirschbaumdevelopment.com/insights/tailwind-transition-colors): Creating stunning web interfaces is as much an art as it is a science. With Tailwind CSS, web developers can elevate their projects by mastering transition colors. This guide will walk you through everything you need to know about Tailwind Transition Colors, from basic setup to advanced techniques.
- [How to Make Laravel Eloquent “WHEREIN” Query. A Step-by-Step Guide](https://kirschbaumdevelopment.com/insights/laravel-eloquent-wherein-query): Master Laravel Eloquent WHEREIN queries with our step-by-step guide. Learn to optimize your database interactions for better performance and efficiency in your web applications.
- [How to set up a new Laravel project](https://kirschbaumdevelopment.com/insights/how-to-set-up-a-new-laravel-project): I recently started a new Laravel project, and as usual, I'm using Inertia, React, and TypeScript. It's been a while since I set up a project from scratch, so I thought I'd put together a quick guide with all the steps you need to get off the ground. If you're like me and prefer this stack, or maybe you're interested in trying it out for the first time, this should help you get started.
- [AI solutions for modern business challenges](https://kirschbaumdevelopment.com/insights/ai-solutions-for-modern-business-challenges): If you've been following the AI conversation lately, you might think Large Language Models are the answer to everything. While tools like GPT-4 and Claude are impressive, treating them as a universal solution is a bit like having a really excellent hammer and seeing everything as nails. Let's explore how different AI technologies can address real business challenges in ways that might surprise you.
- [Avoiding long-lived tokens on AWS](https://kirschbaumdevelopment.com/insights/avoiding-long-lived-tokens-on-aws): Long-lived tokens in AWS are a significant security risk, but AWS provides robust alternatives with IAM Identity Center and OIDC. By using temporary, short-lived credentials, you can significantly reduce the attack surface and maintain compliance with security standards.
- [How to enhance code consistency & efficiency with Laravel Pint](https://kirschbaumdevelopment.com/insights/how-to-enhance-code-consistency-and-efficiency-with-laravel-pint): By integrating Pint into your pre-commit workflows and GitHub Actions, and following a structured approach to reformatting your codebase, you can achieve a cleaner, more maintainable codebase. The benefits of this transition are clear: better readability, easier collaboration, and a unified coding standard that can be enforced across multiple projects.
- [An advanced guide to Laravel Sail](https://kirschbaumdevelopment.com/insights/advanced-guide-to-laravel-sail): Laravel Sail is a great tool for developing Laravel applications without having to set up services on your local machine. It also helps you avoid issues in your application caused by differences between your development and production environments.
- [A practical guide to mutation testing with Pest](https://kirschbaumdevelopment.com/insights/a-practical-guide-to-mutation-testing-with-pest): Mutation testing is a technique that helps us evaluate how effective our test suites are by introducing small, deliberate changes to our codebase. These changes, called mutations, are like mini-simulations of common coding errors or edge cases that test the effectiveness of our test suite.
- [Crafting effective prompts for AI assistants](https://kirschbaumdevelopment.com/insights/crafting-effective-prompts-for-ai-assistants): By providing precise details, you guide the AI to produce results closer to what you want. The bottom line: the effort you put into crafting good instructions directly translates into the quality of the AI’s response.
- [Anatomy of a prompt for AI assistants](https://kirschbaumdevelopment.com/insights/anatomy-of-a-prompt-for-ai-assistants): A well-structured prompt typically includes several key components that work together to guide the AI towards a high-quality response. In this section, we’ll dissect a prompt into its core sections step by step, explain the purpose of each part, and explore how to optimize them.
- [Supercharge Laravel development with AI](https://kirschbaumdevelopment.com/insights/supercharge-laravel-development-with-ai): Explore how powerful AI tools like Curser and Gemini can supercharge your development, especially for Laravel applications.
- [Configuring AWS services for Laravel](https://kirschbaumdevelopment.com/insights/configuring-aws-services-for-laravel): When configuring Laravel to use an S3 bucket, common convention is to use an AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. Instead, we are able to assign the IAM role directly to the EC2 instance. If we leave AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY empty (not included in .env), the IAM role will be used, thus we will still have access to the S3 bucket.
- [Domain-Driven Design is not your folder structure](https://kirschbaumdevelopment.com/insights/what-is-domain-driven-design): Learn the fundamentals of Domain-Driven Design (DDD) and how it helps developers build scalable, maintainable software by modeling real-world business domains.
- [Process is dead. Long live process.](https://kirschbaumdevelopment.com/insights/process-is-dead-long-live-process): Discover why rigid processes are failing modern software teams and how adaptive, outcome-driven approaches can unlock true agility and innovation.
- [Build an MCP server with Laravel Loop](https://kirschbaumdevelopment.com/insights/build-and-mcp-server-with-laravel-loop): Learn how to build a Model Context Protocol (MCP) server with Laravel and Loop. Step-by-step guidance for developers integrating Laravel with AI-powered workflows.
- [Drop in comments for Filament with Commentions](https://kirschbaumdevelopment.com/insights/drop-in-comments-for-filament-with-commentions): Add powerful, real-time commenting to your Filament admin panels. Discover how the Commentions package delivers seamless comments, mentions, and events for Filament v3 & v4.
- [AI and the developer skillset](https://kirschbaumdevelopment.com/insights/ai-and-the-developer-skillset): Explore how AI is transforming what it means to be a software developer. From abstractions to soft skills, learn how emotional intelligence, communication, and technical agility are becoming core to the modern dev toolkit.
- [Human oversight in AI-assisted development](https://kirschbaumdevelopment.com/insights/human-oversight-in-ai-assisted-development): Explore the critical role of human oversight in AI-assisted software development and how Kirschbaum balances artificial intelligence with expert engineering.
- [Why we love React Native and Expo](https://kirschbaumdevelopment.com/insights/why-we-love-react-native-and-expo): Discover why our engineering team chooses React Native and Expo to build fast, scalable, and cross-platform mobile applications for iOS and Android.
## Careers
- [Lead Web Application Developer](https://kirschbaumdevelopment.com/careers/lead-web-application-developer): Seeking a lead/senior developer .
## Full Article Content
### Securing and encrypting a custom-built CRM
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### The need for a custom-built CRM
A specialized consulting agency was managing sensitive payment and salary information through a combination of manual workflows and online spreadsheets. As the organization grew, it recognized the need to consolidate that information into its existing custom-built Customer Relationship Management (CRM) platform. This system would streamline processes such as payment history tracking and approval workflows while maintaining appropriate access controls for sensitive data.
##### Challenge
###### The paradox of access.
The critical hurdle was a **paradox of access**. The agency's existing, simple encryption solution was insufficient for the sensitive financial data they planned to incorporate. Furthermore, the client's internal technical team needed to maintain full administrative access to the server infrastructure to ensure system uptime and maintenance.
However, strict internal policy dictated that this same team should not have access to the sensitive production data itself, even if they accessed the server or database directly. The business needed a way to guarantee a zero-trust environment.
##### Approach
###### Envelope Encryption
We selected an envelope encryption approach to meet the project's security requirements, allowing sensitive data to remain protected even when infrastructure access was available. Because we couldn't find an existing Laravel implementation that met our needs, we developed our own.
In this particular implementation, authorized users in Google can access keys via Google KMS tokens that are stored as encrypted cookies, eliminating any need for persistent data storage and any data leak vectors regardless of infrastructure access levels.
We used the following technologies, software patterns, and encryption concepts to build a custom solution:
1. Envelope Encryption - DEK (key that decrypts the data), KEK (key that encrypts the DEK) on a per database row basis
2. Google KMS (Cloud Key Management Service) - storing key rings and governing access to encryption keys using Google IAM (Identity and Access Management) roles.
3. google/cloud-kms - idiomatic PHP client for working with Google Cloud Platform services.
4. Manager Pattern - allows "driver" swapping (local devs don't need Google Cloud credentials as they aren't required to use the Google Encryption driver). Also opens up the possibility for using other key management services outside of Google.
5. Observer Pattern - ensures database fields are properly encrypted and prevents users who may have access to row data but not encrypted data from accidentally overriding data they were unable to "retrieve".
6. Attribute Casting - custom database field value casting that interfaces with Laravel model observers for decryption and null value handling.
7. Filament - since the existing CRM was written using Filament, we needed to ensure that no data was exposed in the front-end JavaScript.
##### Outcome
###### Risk mitigation and enhanced operational capability.
The primary outcome was immediate risk mitigation and enhanced operational capability.
By using a well understood and industry reliable encryption strategy our client achieved a high degree of confidence that their sensitive data was protected from accidental or malicious exposure, even by their most technical internal staff. This assurance immediately enabled the business to integrate critical processes, specifically their payment history and approval workflow, directly into the new CRM, which was previously deemed too risky.
This integration provided a significant boost to internal efficiency and improved the value (actual and perceived) of the internal CRM. The approach transformed a critical security vulnerability into a core business asset.
##### Reflection
###### Security is not a barrier to business.
This project underscores our philosophy that security is not a barrier to business but an enabler of growth. By focusing on a Technology solution that solves a core Business Outcome (mitigating insider risk while increasing workflow efficiency), we delivered a system that is both highly secure and highly usable. Our expertise lies in architecting custom solutions that navigate the complex intersection of high security, performance, and operational feasibility, ensuring our clients can scale their business operations without compromising confidence.
### Turning SaaS features into competitive advantage
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### High impact features for SaaS business.
Our client operates a rapidly scaling SaaS business that serves industries with seasonal operational peaks. Their business depends on demonstrating ongoing product value through meaningful product improvements that help win and retain customers. They needed a high-impact feature to drive pre-season contract signings and solidify their value proposition.
##### Challenge
###### Performance and intuitive design
Our client’s existing route management tools lacked the performance and intuitive design necessary for their users' service-oriented industry needs. They needed to integrate a custom map route editing feature into their SaaS offering within their existing Filament application. This feature needed to be performant and information-dense, displaying key real-time operational data, but also intuitive enough to be a powerful sales tool and a cornerstone of their user experience. The lack of such a tool limited their ability to demonstrate advanced functionality when compared to competitors, creating friction in their sales cycle.
##### Approach
###### Building an Interactive Route Editor
Leveraging our expertise in modern web technology and the Laravel ecosystem, we designed and built an Interactive Route Editor that could be integrated directly into their established Filament application. The core of the solution was integrating a custom map component.
We ensured the entire system was designed to be performant. We focused on the intricate map route editing functionality, as it requires a lot of quick actions that would become extremely tedious with a sluggish UI. The Filament page acted as a control panel, enabling users to filter and edit route data, which immediately and dynamically updated the map component in real-time. Using AlpineJS and Livewire components gave us great flexibility and power, while also feeling responsive and intuitive, a critical factor for sales demonstrations.
##### Outcome
###### Features as business differentiators.
The introduction of the new Interactive Route Editor immediately translated into tangible business impact. The feature served as a powerful differentiator during our client’s off-season contract negotiations, directly leading to an increase in contract signings for the upcoming peak season.
Furthermore, the feature was met with positive feedback from the existing userbase, who cited the improved performance and clarity of the information display as a significant operational upgrade. Our work delivered both a critical technical component (a high-performance, integrated map tool) and, most importantly, a decisive commercial advantage.
##### Reflection
###### Technology is a lever for business growth
This project underscores our philosophy: technology is a direct lever for business growth. By focusing on a highly performant and intuitive technical solution, we didn't just solve a development problem; we created a compelling sales asset that unlocked pre-season revenue growth for our client. Our ability to blend sophisticated, real-time data handling with a user-centric design approach is what allows our clients to turn product refinements into immediate commercial wins.
### Modernizing an ecommerce subscription platform
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Growth exposed the need for modernization.
A rapidly growing subscription-based ecommerce company built its reputation on delivering high-quality products through a reliable recurring service. As demand accelerated, technology challenges began overshadowing the customer experience that set the business apart.
##### Challenge
###### Limited technical capabilities.
The company's mission-focused strengths were both compelling and evident; however, as demand grew, the platform's technical capabilities hadn't kept pace, creating a gap between the core offering and the customer experience.
As the platform grew, technical debt, aging systems, and limited operational visibility began affecting reliability, maintainability, and the customer experience. The organization needed a stronger technical foundation that could support continued growth while restoring confidence in day-to-day operations.
The fundamental challenge was that technology had become a problem to be noticed, rather than the silent enabler of brand promises kept.
##### Approach
###### Alignment between technical operations and brand integrity.
We embedded a team of senior PHP/Laravel engineers who combined hands-on development with technical leadership and business acumen to stabilize and modernize the platform. Beyond technical delivery, we approached the engagement as an opportunity to transfer best practices in organization, process rigor, and domain expertise. We recognized that sustainable technology reliability requires not just fixing immediate issues, but building internal capabilities that prevent technology from undermining brand promises in the future. This knowledge transfer was impactful and reflected our commitment to long-term alignment between technical operations and brand integrity.
Our key deliverables focused on ensuring that the technology supported, rather than undermined, the brand’s identity:
- **We modernized the application architecture**, establishing a foundation the internal team could more easily understand, maintain, and evolve.
- **We refactored core APIs** to eliminate the brittleness that surfaced as customer-facing unreliability, so the platform could grow without technical limitations becoming customer problems
- **We remediated critical security vulnerabilities** to protect customer data and brand trust.
- **We introduced comprehensive automated testing** to ensure code changes wouldn't surface as customer-facing failures.
- **We introduced automation throughout key fulfillment workflows**, reducing manual effort and improving operational reliability.
- **We improved customer-facing visibility** throughout the fulfillment process, creating a more transparent and reliable experience.
- **We improved operational visibility,** enabling teams to identify and resolve issues more proactively.
Throughout, our guiding principle was to deliver technology solutions that enabled customers to focus on the company’s mission-driven commitments, rather than technology-driven mistakes.
##### Outcome
###### Technology enabling the brand story
The technology transformation successfully moved from being the story to enabling the story:
- **Brand reclaimed the narrative:** fulfillment accuracy improved dramatically, improving customer experience and shifting the focus back to the brand itself
- **Promises aligned with execution:** automated workflows ensured consistent delivery, so the brand's mission-driven positioning was reinforced by reliable experience rather than contradicted by operational failures
- **Transparency became operational reality:** improved internal and customer-facing visibility transformed transparency into lived experience, strengthening brand credibility and enabling proactive problem solving
##### Reflection
###### Think beyond technical execution.
This project demonstrates why mission-driven brands need technology partners who think beyond technical execution. Companies that compete on trust and transparency face a unique challenge in that their brand promises are only as credible as their operational delivery. When you tell customers you're reliable, ethical, and transparent, every delayed order and every fulfillment error becomes evidence against your core positioning.
The success of technology solutions isn't measured by elegant code or modern architecture; it's measured by whether customers experience the brand as it positions itself, consistently and with minimal technical failures.
### Building an investment and trading platform
#### Case Study
**Sector:** Financial Services
---
##### Context
###### A new platform for an established brand.
Our client is a technology-driven financial services company providing investment and trading services across multiple markets. To extend the value of its existing infrastructure, the business aimed to build a complementary client-facing brand focused on developing specialized trading programs, user engagement tools, and optimized platform workflows.
##### Challenge
###### Minimal upfront specification
The primary challenge of the project was in **translating a highly specialized and regulated financial domain into production-ready software with minimal upfront specification.** While the client had deep knowledge of their industry and business, that knowledge existed as operational expertise and not as system specifications, user workflows, or integration requirements.
We needed to extract, interpret, and build simultaneously, transforming complex trading concepts and compliance obligations into robust technical and architectural decisions for an application that **could not launch as a minimum viable product that would improve over time**.
The platform needed to meet demanding operational and regulatory requirements from launch, leaving little margin for error.
##### Approach
###### Progressive partnership
We approached this project the way we approach all complex client projects: **through progressive partnership.** Because complete specifications are rare, particularly in specialized domains, we established a clear rhythm of collaborative, continuous discovery as early as possible.
Our execution model integrated several key practices:
- **Progressive discovery & specification.** We ran weekly standups and on-demand sessions to extract domain knowledge and convert it into actionable technical requirements. Rather than treating the client as a stakeholder reviewing finished work, we positioned them as co-creators, posing clarifying questions via Slack, documenting responses directly in tasks, and refining architectural decisions based on their operational expertise. Direct access to decision-makers enabled us to resolve ambiguous requirements and navigate trade-offs in real-time.
- **Risk-managed integration.** High-stakes systems received structured treatment. We began every integration in sandbox environments, established testing protocols with internal accounts, then validated with client credentials in staging before production deployment. Business risk rules and compliance requirements were documented explicitly and built into validation logic, ensuring regulatory obligations were enforced at the code level, not just business policy level.
- **Continuous validation through working software.** We transparently demonstrated our progress. Regular demos showed actual features running in staging environments where the client could verify behavior firsthand. This approach surfaced misalignments early and confirmed our technical interpretation matched their business intent before code reached production.
- **Incremental, controlled rollout.** Major features were wrapped in feature flags and environment-based toggles, allowing staged activation. Per-user beta access enabled controlled testing with real users before broader release. We deployed to staging automatically on every merge and pushed to production multiple times per week, maintaining rapid iteration without sacrificing stability.
- **Technical debt transparency.** We treated technical debt as a first-class concern, not something to hide. Temporary shortcuts were tracked as explicit follow-up tasks, with corresponding TODOs in the codebase linking directly to tickets. During quieter periods or when touching related code, we systematically addressed this debt, preventing accumulation.
##### Outcome
###### Turning concept into business.
The platform launched successfully and scaled to support a large international user base, handling complex financial transactions and regulatory requirements from day one.
The business impact manifested across several dimensions:
- **Transformed concept into viable business.** We delivered a fully operational platform that turned strategic vision into market reality. What began as a complementary concept alongside their core offering became a standalone growth engine, establishing the client's competitive position in a market segment they previously didn't serve.
- **Demonstrated infrastructure resilience.** Our architectural decisions proved their value under real-world pressure. When an unexpected third-party integration issue arose, the modular architecture and deployment infrastructure enabled a rapid replacement with minimal disruption. Trading data volumes that exceeded projections were absorbed through incremental scaling without disrupting active features.
- **Enabled continuous evolution.** The technical foundation supports ongoing expansion without major refactoring. The client continues to expand the platform through regular releases, supported by the automated pipelines, feature flag infrastructure, and engineering practices established during the initial development. The codebase remains maintainable despite significant feature growth.
- **Validated partnership model.** Our progressive partnership approach established a replicable engagement model. The client has commissioned additional applications from us, and we've applied the same methodology successfully in other regulated financial contexts where requirements emerge collaboratively rather than through upfront specification.
The platform continues operating in production, supporting the client's ongoing growth and evolution.
##### Reflection
###### Intentional software design.
This project demonstrates how our partnership-centered mindset converges with both technical rigor and adaptive execution:
- We didn't have complete specifications, so we built a discovery process that extracted them progressively.
- We couldn't allow integration failures, so we architected for modularity and tested exhaustively.
- We needed to move fast, so we deployed multiple times per week while maintaining stability.
- We faced unexpected challenges, so we built systems resilient enough to absorb them without business disruption.
Every tactical choice reflected our lived experience that uncertainty in software is often the default condition. This is especially true for highly specialized domains. The most effective approach not only acknowledges this reality, but intentionally designs around it.
### Transforming subscription platforms without interruption
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Revamping a photo editing platform.
Our client operates a subscription-based creative platform serving professionals and agencies worldwide. With AI-powered automation and intelligent styling tools, they help teams process large image sets efficiently while maintaining visual consistency.
##### Challenge
###### A limited payment infrastructure.
The client recognized that their payment infrastructure should deliver the same seamless, professional experience that users expected from the rest of their platform; instead, it was doing the very opposite. Subscription quirks and platform limitations were creating problems at every level. Customers encountered billing irregularities and confusing renewal flows. Support teams were managing recurring payment issues. Engineers spent valuable time debugging edge cases instead of building features. Meanwhile, international expansion was accelerating the pressure for an improved platform. Growing internationally introduced increasingly complex tax and regulatory requirements that demanded greater automation and accuracy. The existing system simply wasn't built for this complexity.
The business needed a modern payment solution that could support global growth, operational efficiency, and improved customer experience, but migrating hundreds of active subscriptions without eroding customer trust is a daunting task.
##### Approach
###### Our phased migration strategy.
Kirschbaum designed and executed a phased migration strategy that prioritized risk mitigation, customer satisfaction, and service continuity above all else. Rather than attempting a "big bang" switchover, we built dual-processor support that allowed both systems to run in parallel during a carefully managed transition window.
Our approach centered on:
- **Gentle rollout:** New customers immediately began using Stripe, while existing customers continued uninterrupted on Braintree.
- **Secure data migration**: Working with both processors, we transferred payment tokens and customer data without requiring users to re-enter billing information.
- **Subscription preservation:** Existing subscriptions were recreated in Stripe with precise timing to maintain billing cycles and prevent any service gaps or double charges.
- **Continuous validation:** Each phase included production testing and monitoring before proceeding to the next stage.
This customer-centric approach meant the technical complexity lived entirely behind the scenes, where it belonged.
##### Outcome
###### Migration without downtime.
The migration succeeded on every critical dimension:
- **Hundreds of active subscriptions** transitioned to Stripe without a single customer-facing incident.
- **Zero service disruptions** meant customers continued their work uninterrupted and unaware.
- **Global scalability** was unlocked through Stripe's advanced compliance tools, fraud prevention, automated tax handling, and reporting capabilities.
- **Engineering overhead decreased** enabling energy to be redirected toward product features that directly serve customers.
##### Reflection
###### A foundation for continued growth.
This project embodies our belief that modernizing critical business infrastructure does not require drama or disruption: rather, it requires patience, planning, and unwavering focus on what matters most to the business. By choosing a methodical, phased approach, and by keeping customer experience paramount throughout technical decision-making, we delivered a core system improvement that created a foundation for sustainable, customer-centric global growth.
### Modernizing in-person lead generation technologies
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### In-person lead generation tools.
Our client, a B2B technology company, provides lead generation technology for in-person events. They provide a lead-retrieval solution that allows clients to capture visitor information through event badge scanning. When attendees enter a booth, exhibitors scan their badges to automatically log visits, trigger personalized welcome emails, and alert the right sales representatives, helping to transform foot traffic into qualified sales leads.
##### Challenge
###### Dependence on legacy technologies.
Despite a proven value proposition, the platform's reliance on legacy barcode scanning devices created meaningful constraints:
- A finite inventory of scanners couldn't support multiple simultaneous events or accommodate rapid client expansion.
- The unfamiliar hardware created friction for exhibitors required to learn a new physical user interface.
- Unreliable WiFi at trade show venues meant frequent missed scans and delayed data synchronization.
While the natural solution of building native apps for iOS and Android would solve some aspects of the problem, it also introduced its own challenges. Dual native development meant higher upfront costs, ongoing maintenance of separate codebases, version fragmentation across user devices, and lengthy app store approval cycles for updates. For a business focused on removing barriers to engagement, these tradeoffs worked against the fundamental mission.
##### Approach
###### Designing a Progressive Web App
To address the client’s needs, we designed and built a Progressive Web App (PWA).
A PWA is a web application that combines the best of websites and native apps. Users access it through a browser URL, but it can send notifications and be installed on the home screen just like a regular app. Our technical approach leveraged patterns and features common to PWAs, but strategically prioritized data resilience to handle the unique challenges of trade show environments where connectivity is unpredictable and data loss is unacceptable.
The architecture centered on four key capabilities:
- **Service worker implementation**: Service workers intercept network requests and cache critical assets, managing seamless transitions between online and offline states so users experience no interruption in functionality.
- **Local data persistence**: IndexedDB stores all scanned products and user selections directly on the device, ensuring zero data loss even during extended connectivity outages.
- **Background sync with retry logic**: A smart synchronization system queues local changes and uploads them when connectivity is restored, with automatic retry logic that prevents data loss even if uploads are interrupted.
- **Native camera integration**: Browser APIs access device cameras for barcode scanning, delivering the same functionality as dedicated hardware through familiar smartphone interfaces.
##### Outcome
###### Upgraded scalability and functionality.
The PWA solution delivered measurable technical improvements that directly addressed the client's operational constraints:
- **Improved scalability:** The solution supports more concurrent users since it runs on attendees' own devices rather than requiring a fixed inventory of specialized hardware.
- **Reliable offline functionality:** The offline-first architecture with local data persistence and resilient syncing ensures reliable capture of every scan regardless of venue connectivity.
- **Reduced maintenance overhead**: The single codebase and web-based distribution model reduced development costs and enabled rapid iteration when compared to what dual native apps would have required.
- **Familiar user experience:** Browser-based camera access delivered the same barcode scanning functionality as dedicated hardware, removing the learning curve and device management burden.
##### Reflection
###### Balancing pragmatism with innovation.
This project exemplifies our commitment to balancing pragmatism with innovation in the solutions we deliver. By understanding our client as well as the technical constraints of trade show environments and the economic realities of scaling a specialized software business, we delivered a solution that removes growth barriers while enhancing the core product experience. Progressive Web Applications, when thoughtfully architected, can deliver transformative business outcomes without the overhead traditionally associated with enterprise-grade mobile solutions.
### Standardizing ecommerce infrastructure at scale
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### A rapidly growing ecommerce marketplace.
Our client is a pioneering ecommerce marketplace providing electronics, fashion, and home goods. While their rapid growth produced tangible business value, it also increased technological and operational complexity. Their platform ecosystem consisted of dozens of Laravel applications and supporting services deployed through a mix of managed platforms, third-party tools, and manually configured AWS resources. While this approach enabled early speed, it increasingly limited the organization’s ability to reason about its infrastructure, enforce consistent security standards, and scale operations confidently as the business and team grew.
##### Challenge
###### Standardizing technology after organic growth.
The client’s core business challenge centered around regaining control and consistency across an ecosystem that had evolved organically over nearly a decade. Several critical issues compounded the problem:
- Infrastructure and configuration were sprawled across multiple AWS accounts and tools leading to configuration drift and unclear ownership.
- Years of legacy resources and ambiguous configurations made it difficult to understand what existed, what was used, and what posed risk.
- Each workload category – including core applications, mini applications, and analytics tools – used a different deployment method.
- The internal team lacked Kubernetes expertise, but the platform needed container orchestration for scalability and reliability.
- Security requirements demanded zero hardcoded AWS credentials, relying exclusively on SSO, OIDC, and IAM roles for both development and deployment.
The client needed a unified, reproducible way to deploy and operate all workloads without introducing operational fragility or slowed delivery.
##### Approach
###### Designing AWS-native architecture.
We partnered closely with the client’s engineering team to design an AWS-native architecture focused on standardization, operability, and security by default.
**Infrastructure as code & standardization**
- Introduced infrastructure-as-code using Terraform and Terragrunt as source of truth.
- Replaced ad-hoc AWS configuration with reusable, tested modules.
- Standardized security, networking, logging, and environment isolation across all applications.
**Containerized application platform**
- Containerized core Laravel applications.
- Chose AWS ECS Fargate over Kubernetes to deliver container orchestration with significantly lower operational overhead.
- Enabled Laravel Octane on Fargate using optimized container images for high-performance workloads.
**Secure, automated delivery**
- Implemented GitHub Actions–based continuous deployment pipelines for all environments.
- Used OIDC authentication between GitHub and AWS, eliminating static access keys entirely.
- Centralized secrets management using AWS Secrets Manager and IAM-based access controls.
##### Outcome
###### Reduced risk, improved confidence, standardized infrastructure.
The platform transformation delivered business impact by reducing operational risk, improving delivery confidence, and establishing a standardized infrastructure foundation that scales with the organization.
**Improved delivery reliability & velocity**
- Faster, more predictable releases enabled by standardized infrastructure and deployment workflows.
- Protected customer experience and revenue through zero-downtime deployments.
- Reduced coordination overhead between teams, accelerating feature delivery and time-to-market.
**Lower risk & stronger security**
- Reduced security exposure through the elimination of hardcoded credentials and adoption of role-based access.
- Improved auditability and governance by removing configuration drift and undocumented infrastructure changes.
- Consistent security enforcement across environments, independent of individual applications or teams.
**Reduced operational overhead at scale**
- Improved platform operation through a unified, container-based deployment model.
- Lower cognitive load for engineers, enabling greater focus on product and business priorities.
- Easier onboarding and clearer ownership as the team and platform continues to grow.
##### Reflection
###### Infrastructure decisions have operational impacts.
This project reinforced that infrastructure decisions have direct operational consequences. When standardization, security, and observability are built into the platform as first-class concerns, teams spend less time untangling infrastructure inconsistencies and more time building what matters. The result is not just technical stability, but organizational clarity around what’s running and how to manage it.
### Orchestrating real estate settlements across incompatible systems
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Delivering a seamless experience for clients.
Our client is a settlement services provider that manages the technical and administrative complexities of real estate transactions for financial institutions. The banks and credit unions that they partner with rely on them to coordinate appraisals, process title work, and facilitate closings that keep loans moving toward settlement. They needed a system that could communicate across various APIs and orchestrate the delivery of a seamless service experience for their clients.
##### Challenge
###### Orchestration across incompatible systems.
The fundamental challenge was implementing orchestration across multiple incompatible systems. Data needed to be kept safely synchronized across several third party systems and monitored for updates to provide rapid delivery to the customers.
The circumstances created operational challenges across two dimensions:
**Manual integration and fulfillment friction**
- Loan orders needed flexible entry points for creation, allowing lenders to place orders in a way that best fit their system.
- Fulfillment status required a unified monitoring layer instead of being spread across multiple vendor platforms.
- They needed an automated approach to extracting appraisal and title results, with the ability to push documents and data back into lender systems.
- High throughput and reliable transfer (removing "human error") were necessities as volume grew.
**Lack of orchestration across products and parties**
- Different loan products required workflow awareness to account for unique sequencing and dependencies.
- Work needed to be intelligently routed and progressed based on product-specific rules.
- Communication needed to live inside the system rather than relying on off-platform emails and calls.
- Without a centralized orchestration layer, visibility would be fragmented and time-sensitive issues harder to resolve.
##### Approach
###### Building an intelligent orchestration layer.
We designed and built a comprehensive integration platform that functions as an intelligent orchestration layer between lenders, the core settlement system, and fulfillment vendors. The architecture needed to accommodate the reality of diverse systems and technologies. Some lenders have sophisticated APIs, others have limited technical capabilities, or special business requirements; some vendors support webhooks, others require polling.
The solution provides multiple pathways for order creation: an API where lenders can post orders directly from their loan management system or custom integration components for creating orders directly within their existing workflows. Once orders enter the system, a sophisticated monitoring infrastructure tracks products through fulfillment using webhooks where available, polling where necessary, and a comprehensive status management system that understands product-specific workflows. When an appraisal or title search is needed, the platform automatically routes work to appropriate vendors, then retrieves and processes results without human intervention.
The bidirectional integration architecture pushes completed deliverables back into each lender's system via their specific APIs. For lenders who prefer a pull model, we exposed endpoints where they retrieve fulfilled orders on their schedule. To eliminate the communication gap, comments can be attached directly to orders and flow through the same integration channels, keeping all stakeholders informed within their preferred systems.
##### Outcome
###### Technology transforms business operations.
The platform fundamentally transformed the company's operational capacity. What previously wouldn't have been possible at increasing volumes.
- **Client experience improved:** lenders gained real-time visibility into order status through their own systems, reducing support inquiries.
- **Onboarding barriers reduced:** integration flexibility meant new lender clients could be onboarded regardless of technical sophistication or existing software.
- **Order processing accelerated:** data flows directly between systems without manual handoffs or re-entry, eliminating bottlenecks and human error.
- **Scalability unlocked:** the architecture handles diverse integration patterns, product workflows, and vendor relationships through configuration rather than custom code, meaning growth doesn't require rebuilding core systems.
##### Reflection
###### Technical solutions make complexity invisible.
This project exemplifies our belief that the best technical solutions make complexity invisible to users while remaining flexible enough to accommodate business reality. Rather than forcing all parties into a single integration pattern, we built infrastructure that meets systems where they are: webhook or polling, push or pull, automated or manual.
The result is a platform that delivers business value through intelligent automation while maintaining the adaptability essential in an industry where every client operates differently. By treating integration as a core competency rather than an afterthought, we helped our client transform an operational bottleneck into a more scalable operational foundation.
### Enhancing education with AI-powered chat
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Faster, clearer research
A leading aromatherapy educator approached Kirschbaum with a challenge: students in their certification programs needed a faster, clearer way to research essential oils, safety guidance, and recipes using their extensive catalog of in-depth articles, chemical component reports, and third-party materials.
The client envisioned a conversational reference tool where students could ask questions in plain language – for instance, “Which oils contain linalool?”, “Can I diffuse this around pets?”, or “What’s a good blend for sleep?” – and receive accurate, cited answers grounded in their existing educational content.
Kirschbaum partnered with their team to bring that vision to life.
##### Challenge
###### Building an AI-powered reference assistant.
The primary challenge was to design and implement an AI-powered reference assistant that met strict accuracy, traceability, and safety requirements across a complex content ecosystem:
- Content existed in multiple publishing systems with no unified search layer.
- Every answer had to cite and link back to original sources to ensure trustworthiness and academic rigor.
- Students asked questions spanning chemical components, safety warnings, and practical usage scenarios, requiring the system to interpret nuanced domain concepts reliably.
- The assistant needed to function as an embedded feature within a paid educational platform without disrupting existing workflows.
- Due to the health-related nature of aromatherapy, the tool had to include contextual guardrails (e.g., use around children or pets) so that answers remained safe and compliant with best-practice guidance.
Meeting all of these constraints while maintaining usability, speed, and reliability demanded a carefully engineered AI and data infrastructure.
##### Approach
###### A phased, engineering-driven engagement model.
We partnered with the client in a phased, engineering-driven engagement that delivered value incrementally while mitigating risk. Our process involved:
**1. Beginning with a tightly scoped proof of concept.**
We began by ingesting and indexing the client’s core article library into a vector database using OpenAI models. Content was split into granular chunks to give the AI precise access to individual sections rather than broad summaries, improving relevance and accuracy.
**2. Expanding knowledge sources dynamically.**
Alongside internal content, we integrated external partner blog systems and enabled ongoing ingestion of new educational materials. This ensured the assistant’s knowledge base stayed current and comprehensive.
**3. Building a robust backend for retrieval and response shaping.**
Using a Laravel-based backend, we implemented:
- Vector search and document retrieval.
- AI querying and response construction.
- Automated article synchronization.
- Embeddable delivery within the client’s existing platform.
**4. Enforcing rigorous citation handling.**
To ensure every answer could be traced back to original sources, responses weren’t streamed as raw AI output. Instead, the system assembled and enriched each answer with complete citations before delivering a polished, unified message.
**5. Delivering within the student ecosystem.**
By embedding the AI assistant directly into the client’s platform interface, we made it seamlessly accessible to learners without introducing new tools or workflows.
##### Outcome
###### Improved educational engagement
The solution delivered a clean, conversational AI assistant that transformed how students engage with educational content. Key features included:
- **Natural-language questioning:** Students can now ask questions in plain English about essential oils, chemistry, safety, and practical usage.
- **Fast, reliable answers:** Rather than manually searching hundreds of articles, users receive concise answers with verifiable citations attached.
- **Practical guidance:** The assistant supports “recipe”-style queries, helping students explore specific use case combinations with contextually appropriate safety guidance.
- **Embedded experience:** Delivered as a native feature of the existing educational platform, the tool integrates smoothly into the student journey without requiring additional logins or separate apps.
Educational engagement improved as learners could research faster, trust the information they received, and draw connections across sources more efficiently.
##### Reflection
###### Applying AI responsibly.
This project highlights several core insights into applying AI responsibly in domain-specific educational contexts:
- **Accuracy must be engineered, not hoped for.** Citation-driven responses were critical for user trust in a field where misinformation can have real-world implications.
- **Data integration underlies AI value.** A powerful frontend experience depends on a well-structured, comprehensive content ingestion and retrieval system.
- **Seamless integration maximizes adoption.** Embedding the tool within the existing student platform ensured usage didn’t require learners to adopt new workflows.
- **Iterative delivery de-risks complexity.** Starting with a proof of concept and expanding incrementally allowed us to validate assumptions and refine the model before broader deployment.
The result is a student-focused product that bridges rich educational content with intuitive AI assistance, enhancing learning without compromising reliability or safety.
### Revolutionizing a lead-generation platform with AI
#### Case Study
**Sector:** Legal
---
##### Context
###### Reducing mismatched leads.
Our client, a rapidly growing digital lead-generation platform, was experiencing a significant operational bottleneck: a high rate of mismatched leads. Prospects often self-selected categories that didn’t align with the actual service needed, resulting in wasted time for both clients and their potential customers, lower satisfaction, and reduced conversion performance.
The business engaged us to redesign its lead intake and classification system with the goals of:
- **Intelligently classifying inbound leads** using conversational inputs rather than rigid form fields.
- **Reducing mismatches and inefficiencies** so leads were routed to the right teams from the start.
- **Supporting flexible experimentation** with evolving AI models and prompts without destabilizing the core user experience.
##### Challenge
###### From traditional forms to AI systems.
The central challenge was transforming a traditional, form-driven intake flow into a dynamic, AI-assisted system capable of handling varied, nuanced user descriptions while preserving accuracy and scalability.
- **High mismatch rate:** The legacy form logic often failed to interpret nuanced language and led to incorrectly classified opportunities.
- **Rich domain complexity:** Industry issues varied widely, with localized regulatory and terminology differences that simple rules-based systems couldn’t handle.
- **Limited experimentation pipeline:** The existing architecture lacked a safe and controlled way to test and compare evolving AI models or iterative prompts.
- **Rigid submission methods:** Traditional web forms limited alternative interaction channels (e.g., conversational UI or voice) and hindered user experience innovation.
The solution needed to enhance lead quality and routing accuracy while enabling ongoing experimentation and evolution of the system’s AI components.
##### Approach
###### Structured, iterative engineering.
We tackled this challenge with a structured, iterative engineering approach that balanced user experience, flexible experimentation, and robust classification logic:
**Intelligent AI-driven classification system**
We replaced static form fields with an AI-powered classifier that analyzes user-submitted issue descriptions and assigns leads to the appropriate category. The system augments classification prompts with similar past leads retrieved via vector search and retrieval-augmented generation (RAG) to boost accuracy and contextual understanding.
**Parallel model experimentation framework**
To support continuous improvement, we implemented a pipeline that runs multiple candidate models in parallel against live and historical data. This enables real-time comparison of accuracy, response time, and effectiveness, allowing the team to gradually refine, validate, and deploy improved models without disrupting production traffic.
**Iterative prompt tuning and model updates**
We established a cadence of ongoing prompt and model refinement to adapt to evolving user language and domain nuances. Successful new versions are rolled into production once they consistently outperform existing ones in controlled tests.
**Conversational, scalable intake flows**
Replacing cumbersome form flows with an interaction model that feels conversational reduced friction for prospects and improved the quality of lead data collected. Future expansions like voice-based intake channels were architected into the platform from the outset.
##### Outcome
###### Reduced mismatched leads. Increased operational performance.
The revamped lead-generation backbone delivered measurable improvements across operational performance, revenue outcomes, and user satisfaction:
**Improved lead routing accuracy**
AI-assisted classification improved lead routing accuracy, reducing unnecessary handoffs and allowing clients to focus on opportunities aligned with their expertise.
**Clear revenue uplift**
Better-matched leads and workflow efficiencies translated into meaningful revenue gains, demonstrating strong ROI from the upgraded intake pipeline.
**Improved client and prospect experience**
Clients reported that wasteful follow-ups declined and prospects were engaged in a more intuitive, conversational way, increasing satisfaction for both sides.
**Future-ready, scalable architecture**
The parallel experimentation framework positions the platform to quickly adopt new AI advancements, experiment with voice interfaces, and roll out new categorization logic efficiently.
Across outcomes, the platform is now equipped to scale intelligently while keeping lead quality high and innovation pathways open.
##### Reflection
###### Technology transforms business processes.
This engagement illustrates how adaptive AI systems combined with thoughtful engineering and experimentation infrastructure can transform a core business process:
- We replaced brittle form logic with intelligent classification models that interpret conversational input more accurately.
- We mitigated risk by building parallel testing and rollout mechanisms that allowed experimentation without production disruption.
- We enhanced user experience by prioritizing conversational interaction flows over rigid questions.
- We laid a scalable foundation that supports ongoing model improvements and new interaction channels like voice.
By engineering for flexibility and continuous learning rather than fixed rules, this project positions the lead-generation product to remain competitive and effective in a rapidly evolving AI landscape.
### Transforming aromatherapy education and retail
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Building a flexible platform for content and retail.
Our client operates a dynamic online learning platform focused on aromatherapy education. Over time, the business encountered growing complexity in managing both educational content delivery and retail operations through separate systems. The technical landscape included custom solutions for student networking, blogging, courses, and payments, but lacked the flexibility to easily create and update marketing and application content without developer intervention.
The organization also runs an ecommerce platform selling essential oils and related products via Shopify. While functional, the existing Shopify setup limited customization and aesthetic control, constraining the client’s ability to present a cohesive brand experience.
The client engaged Kirschbaum to build a flexible, integrated platform that would support rapid content creation, enhance the learning experience, and elevate ecommerce presence.
##### Challenge
###### Creating flexibility without compromising architectural quality.
This initiative involved two interrelated yet distinct technical challenges:
**Dynamic content and education delivery:** The client’s educational platform required support for student networking, blogging, payment workflows, and course resources. Off-the-shelf solutions either lacked necessary features or were too rigid, forcing the team into time-intensive direct HTML edits for new pages and marketing materials. Non-technical staff needed the ability to build and modify web content quickly without developer support.
**Ecommerce appearance and flexibility:** The client’s Shopify-based store needed a significant upgrade. They wanted improved visual branding, enhanced customization options, and greater control over user experience. However, Shopify’s native tooling limited layout flexibility, making certain customizations difficult to achieve without engineering effort.
The combined solution needed to support content agility, a polished student user experience, and a customized retail storefront in systems accessible to non-developers without sacrificing architectural quality.
##### Approach
###### Platform flexibility with modular design.
To meet these goals, we employed a multi-faceted strategy focused on platform flexibility, modular design, and enhancing non-technical editing capabilities:
**Custom dynamic content builder:** We introduced a powerful web builder framework, similar in concept to platforms like Shopify, but built on a custom architecture using Filament. This enabled the client to generate and modify content, including marketing pages, application flows, and student resources, without writing HTML or requiring developer intervention.
**Configurable, user-friendly architecture:** Rather than hard-coding layouts and components, we built a highly configurable system that allows the client’s team to assemble pages and features through intuitive interfaces. This approach reduced error risk, increased responsiveness to marketing and educational needs, and shortened iteration cycles.
**Elevated ecommerce experience:** For the storefront, we modernized the Shopify implementation, revamping site appearance and functionality. Leveraging Laravel where needed, we extended customization capabilities, introduced additional styling options, and aligned the retail experience with the education platform’s brand vision.
**Retheming and visual alignment:** We executed a comprehensive retheming of the marketplace, stretching the limits of Shopify’s customization capabilities while maintaining a sustainable, maintainable codebase that the client could evolve over time.
Kirschbaum partnered closely with the client to ensure deliverables matched business goals, supported rapid iteration, and reduced the technical barrier for future content updates.
##### Outcome
###### Empowering teams and enhancing user experience.
The project delivered a flexible, unified platform that empowered the team and enhanced end-user experiences:
**Empowered content creation:** Non-technical staff can now create and modify pages without developer assistance, significantly reducing turnaround time for marketing campaigns, resource updates, and promotional content.
**Improved educational experience:** Students enjoy a faster, more seamless learning environment with a system designed around dynamic content delivery rather than static, hard-coded pages.
**Upgraded retail presence:** The ecommerce site now reflects a more customized, visually compelling retail experience. The transition to Shopify 2, combined with aesthetic and functional enhancements, elevated the brand’s digital storefront.
**Reduced technical friction:** By removing the need for manual HTML changes and enabling configuration through intuitive interfaces, the client reduced development bottlenecks and technical friction for non-engineer team members.
Overall, the platform’s modularity and configurability support ongoing business evolution without heavy reliance on engineering resources.
##### Reflection
###### Developer-friendly solutions that amplify business autonomy.
This engagement highlights our ability to deliver practical, developer-friendly solutions that amplify business autonomy and user experience:
- **We shifted content control back to the team:** Non-developers can now build and update pages quickly, increasing agility and reducing dependency on engineering support.
- **We balanced customization with maintainability:** The solution supports visual richness and flexibility without compromising architectural quality or future extensibility.
- **We unified disparate systems:** By enhancing both the education and retail sides under consistent patterns, the client now operates a more cohesive digital ecosystem.
- **We delivered a future-ready platform:** The technical foundation supports rapid iteration and aligns with the client’s growth trajectory in a competitive market.
The result is a robust, user-centric platform that strengthens the client’s ability to educate, engage, and sell while minimizing technical bottlenecks.
### Revitalizing a music production marketplace
#### Case Study
**Sector:** Commerce & Logistics
---
##### Context
###### Re-engineering, refining, relaunching.
Our client operates a community-based music production and distribution marketplace built to empower producers and artists to collaborate, create, and share music while retaining full ownership of their work. The platform blends creative community tools with commerce, enabling independent creators to engage with each other and the broader market.
Despite its strong mission and early adoption, the application faced architectural limitations, inconsistent feature sets, and usability challenges that threatened its scalability and long-term viability. The CEO engaged us to re-engineer the product’s core systems, refine its feature set, and position the platform for sustainable growth in a dynamic industry.
##### Challenge
###### Transforming an evolving but fragile application
The primary challenge was transforming an evolving but fragile application into a robust, scalable, and feature-rich marketplace capable of delivering a modern user experience and supporting future expansion. To accomplish this, the team needed to overcome several critical obstacles
- **Outdated architecture:** Existing systems were brittle, hindering development velocity and making feature rollouts unpredictable.
- **Subscription limitations:** The original subscription management workflows were inflexible and lacked clarity, negatively impacting conversion and retention.
- **Inefficient commerce flows:** Order and purchase processes were cumbersome, leading to friction between users and service providers.
- **Limited media handling:** Uploaded audio files weren’t reliably playable across environments, weakening the platform’s core media experience.
- **Communication gaps:** There was no native way for users to discuss projects or negotiate purchases in context, which reduced engagement.
Meeting these challenges required both **technical re-engineering and strategic feature enhancements** to transform the product into a resilient, user-centric digital marketplace.
##### Approach
###### Re-imagining with an architecture-first mindset.
We partnered with the client to reimagine the platform with an **architecture-first mindset**, focusing on scalability, usability, and long-term maintainability. To realize this vision, we executed a series of targeted improvements:
**Architectural overhaul & feature prioritization** We audited the existing platform to identify core capabilities that would deliver immediate value and support future growth. This process informed a phased architectural rewrite that stabilized the foundation and enabled modular delivery of features.
**Subscription management rewrite** Subscription logic was redesigned from the ground up to enhance flexibility and user experience. New workflows supported clearer plan structures, billing logic, and administrative controls.
**Commerce and UI improvements** The order and purchase experience was rebuilt with user-centered flows. We designed smoother pathways for discovering services and buying from providers, accompanied by an updated UI that elevated aesthetics and clarity.
**Built-in communication tools** We introduced a **chat feature** enabling real-time dialogue between creators and buyers, facilitating negotiation and collaboration without external tools.
**Reliable media conversion & streaming** To ensure a consistent listening experience regardless of network conditions, we implemented **an automated media processing pipeline**, automatically translating uploaded audio into streamable m3u8 formats.
**Robust order management & payments** A comprehensive order management system with detailed status tracking and event hooks was constructed. We also integrated Stripe for secure, scalable payment processing, directly supporting transactional reliability.
**Affiliate program implementation** To expand reach and incentivize referrals, an affiliate program was launched, enabling external promoters to benefit from network growth.
##### Outcome
###### From brittle to scalable.
The platform was transformed from a brittle prototype into a **feature-rich, scalable marketplace** capable of supporting growing user demand and evolving business priorities:
- **Stable, scalable foundation:** A re-engineered architecture now supports continuing development without systemic bottlenecks.
- **Enhanced subscription experience:** Users enjoy clearer, more flexible subscription options, contributing to better retention and monetization.
- **Seamless commerce:** Redesigned purchase flows and UI improvements reduced friction and boosted overall conversion efficiency.
- **Integrated communications:** Built-in chat increased user engagement and made transactions more interactive and transparent.
- **Reliable streaming:** The new media conversion system ensures all uploaded music streams smoothly regardless of connection speed.
- **Scalable payments & expansion:** Stripe integration and affiliate support strengthened revenue capture and opened new growth channels.
Together, these improvements repositioned the client as a **sophisticated, adaptable platform** ready to compete in a rapidly evolving digital music ecosystem.
##### Outcome
###### Disciplined engineering unlocks sustainable scale.
This project demonstrates how disciplined architecture and **user-centric feature design** can revitalize a marketplace application and unlock sustainable scale:
- **We stabilized and future-proofed the product** by re-architecting its core systems.
- **We elevated key user flows** from subscription to commerce and communication, making the experience more intuitive and efficient.
- **We embraced strategic extensibility**, building components like a media conversion pipeline and affiliate program that support long-term business goals.
The resulting platform not only meets current user needs but also provides a scalable foundation for future innovation in the competitive music production marketplace space.
### Enabling global marketing for a pharmaceutical company
#### Case Study
**Sector:** Life Sciences & Pharma
---
###### Developing a marketing platform for a pharma company.
Kirschbaum was tasked with building a greenfield platform to enable marketers of a global pharmaceutical company to create emails, static websites, and print materials using a user-friendly, drag-and-drop interface. From the project's inception, our developers served as the primary development team, establishing a modern foundation using Laravel. The platform was designed to empower non-technical users to build custom, reusable templates through "variables" libraries and pre-defined modules. A critical requirement was the integration of local legal text modules and specialized tools for pharmaceutical sales, ensuring compliance across diverse global demographics.
###### Navigating global teams and regulations.
The project began with a small, focused team but quickly scaled, requiring us to guide several medium-sized teams on a single codebase. This rapid growth necessitated clear application boundaries and "bridges" of shared code to prevent team conflict and overlap.
The complexity was compounded by a progressive global rollout across multiple international regions with vastly different pharmaceutical advertising laws.
As the team expanded, institutional knowledge within a small group of senior engineers became a bottleneck, creating high onboarding friction. At the same time, the organization was standardizing its Agile delivery practices, adding another layer of coordination alongside the technical execution.
###### Leveraging Domain Driven Design.
We leveraged Domain Driven Design (DDD) to structure the project, which allowed us to maintain a sharp focus on the scope of features like reusable, categorized templates and template variable libraries. Our technical strategy was underpinned by Test Driven Development (TDD), implementing robust backend functional unit testing and automated end-to-end browser testing for critical user workflows.
To manage the intersections of infrastructure and software across multiple teams, we maintained a comprehensive directory of external team contacts, mapped to each project's scope. We prioritized process standardization by establishing Agile ceremonies, Jira ticket templates, and Git commit guidelines. Recognizing that tribal knowledge was a risk to scalability, we adopted a "documentation first" mentality, developing feature-specific documentation to streamline onboarding. Regular cross-team leadership meetings and standardized Git workflows were instrumental in resolving information gaps and code conflicts early in the development cycle.
###### Cutting time-to market and scaling globally.
The technology transformation delivered significant operational improvements and global scalability:
- **Drastic time-to-market reduction:** By iteratively improving features, we reduced the marketing material approval cycle from two months to just four days.
- **Compliance automation:** We built a real-time validation system that highlights unaddressed legal requirements during the design process, ensuring compliance is built-in rather than an afterthought.
- **MLR optimization:** We developed an evaluation system that helps marketers identify content likely to require additional revisions before Medical-Legal Review (MLR), providing actionable suggestions prior to submission.
- **Global scale:** The unified platform successfully supports over a dozen countries across multiple continents, accommodating regional variations within a single, maintainable codebase.
###### Communication as the key to code.
This project highlighted that as a team scales, communication becomes as critical as the code itself. Regular cross-team meetings and open, feature-specific documentation are not just "nice-to-haves"; they are essential infrastructure that supports development velocity.
We also learned the importance of architectural flexibility; for instance, a "Positioned Modules" interface would have been more robust than our initial "Module Layers" approach for non-layered designs. Ultimately, the success of the platform was rooted in early standardization of Git workflows and Agile ceremonies, which allowed us to transition from a small "tribal" team to a high-performing, multi-team organization that delivers global impact.
### Enterprise-grade authentication for a healthcare platform
#### Case Study
**Sector:** Healthcare
---
###### The need for enterprise-grade identity management
Our client operates in the healthcare technology space, developing proprietary software and hardware systems used to monitor patient rooms and support clinical staff workflows. As the platform grew, so did the expectations of the healthcare organizations evaluating it, particularly larger enterprise hospital systems with increasingly strict security and identity-management requirements.
At the time, the platform consisted of three separate applications with independent authentication systems and inconsistent user experiences. Logging into the broader ecosystem required users to move between disconnected applications, each with its own session management and authentication behavior. This fragmented experience created operational friction for healthcare staff and became a growing limitation during enterprise sales conversations.
To support continued growth into larger healthcare environments, the client needed a unified authentication strategy that could support centralized login, multi-factor authentication (MFA), and customer-specific single sign-on (SSO) integrations, all while maintaining the reliability expectations required in clinical settings.
###### Reliability requirements shaped every technical decision
This project carried a unique set of operational constraints that significantly influenced the architecture of the solution.
Because the platform is used in healthcare environments tied to patient room monitoring, certain common web application behaviors were unacceptable. User sessions could never be unexpectedly invalidated during deployments, authentication changes could not interfere with active workflows, and login flows needed to avoid full-page refreshes that could create uncertainty for clinical staff.
These constraints meant the project could not simply rely on "standard" authentication scaffolding or off-the-shelf implementations. The authentication system needed to feel seamless and highly resilient while operating across multiple independent applications with different customer authentication requirements.
###### Building a unified authentication platform
We designed the authentication architecture around two competing realities: increasingly strict enterprise security expectations and the operational continuity required inside clinical environments.
- A shared authentication platform for seamless cross-application login
- Multi-factor authentication (MFA) flows designed to minimize workflow disruption
- Customer-specific SSO integrations for enterprise healthcare organizations
- Session management strategies designed to avoid forced logouts during deployments
- Front-end authentication flows designed to avoid hard page refreshes and preserve application continuity
Because different healthcare customers maintained different identity providers and security requirements, the architecture needed to remain flexible and extensible. The resulting implementation allowed the platform to support multiple enterprise authentication models without requiring customer-specific forks or fragmented authentication logic.
The system was engineered around operational continuity as much as security. Authentication flows were designed to behave predictably during deployments, infrastructure changes, and session transitions, reducing the likelihood of interruptions in environments where reliability is critical.
###### Security and usability are not competing priorities
The primary outcome was a substantial improvement in the platform's enterprise readiness.
By unifying authentication across the ecosystem and introducing enterprise-grade MFA and SSO capabilities, the client was able to meet the procurement and security expectations of larger healthcare organizations that previously would have been difficult or impossible to support.
At the same time, the platform delivered a significantly improved day-to-day experience for end users. Clinical and operational staff could move between applications through a more seamless authentication experience without disruptive login behavior or workflow interruptions.
The project ultimately transformed authentication from a fragmented operational weakness into a strategic capability that directly supported larger enterprise opportunities.
###### Architecting for high-trust environments
This project reinforced an important principle: in high-trust environments like healthcare, authentication is not simply a security feature; it is part of the operational reliability of the product itself.
By carefully balancing enterprise security requirements with real-world usability and deployment constraints, we helped deliver an authentication platform capable of supporting both the technical and operational expectations of modern healthcare environments.
Our focus was not simply implementing MFA or SSO. It was designing an authentication experience resilient enough to operate in environments where reliability, continuity, and trust are essential.
### Rebuilding provider configuration for a healthcare staffing platform
#### Case Study
**Sector:** Healthcare
---
###### Rebuilding a custom field configuration system
Our client operates a platform used by hospital systems to manage physician workforce operations. As they onboarded new hospital corporations, each came with its own requirements for how billing codes and provider data should be structured, requirements that varied significantly across health systems. The platform supports a large network of healthcare organizations, managing clinician, contract, and billing data across hundreds of facilities and hundreds of thousands of records. Every configuration adjustment previously required developer involvement, a recurring bottleneck that slowed growth. Our engagement began when our client brought us in to complete the rebuild of their custom field configuration system, giving their operations team the ability to manage per-hospital settings directly, without engineering support.
###### Data entanglement
The feature had a working foundation when we joined, and we spent time building a thorough understanding of it before extending further. As we did, the full scope of what the system needed to handle became clear.
Legacy data was structured differently across multiple organizational models, with information flowing through several interconnected payment, reporting, and contract management processes. Supporting those existing workflows while introducing a more flexible configuration system required careful planning to preserve data integrity throughout the transition. Along the way, related efforts, including specialty management and integration with the client's existing operational workflows, grew into significant parts of the overall project.
###### Embedding into the client's team
We embedded directly with our client’s internal team, taking clear end-to-end ownership of the new feature while their developers maintained the rest of the platform. This division of responsibility let us move quickly while staying tightly coordinated at the points where the new system connected to payments and reporting.
The centerpiece of the rebuild was a self-service management portal that lets our client’s operations staff configure data fields per hospital, setting validation rules and defining whether fields apply to employed or independent physicians with no developer involvement required. To make the transition from the legacy system low-risk, we built in controls that let the client configure, test, and validate each hospital's setup before going live, with each hospital enabled independently on its own timeline. Built-in migration tools streamlined the transition from the legacy system while reducing manual effort and helping ensure data integrity.
We also built a historical records system that preserves key configuration values at the point of approval, ensuring reports continue to reflect the state of the data when decisions were made rather than current configuration settings.
###### Streamlining processes and organizing data
The rebuilt system delivered across several dimensions simultaneously:
- **Self-service configuration**: Client's operations staff can now define and adjust data fields for any hospital through the management portal, work that previously required a developer on every request.
- **Safer, faster onboarding**: New hospital corporations can be configured and enabled without a code deployment. Each goes live on its own schedule, independently of the others, giving operations full control over the rollout.
- **Accurate reporting**: Historical snapshots ensure reports continue to reflect approved data as it existed at the time of key business events, eliminating inconsistencies caused by later configuration changes.
- **A scalable foundation**: Moving from a rigid, format-specific data structure to a flexible one eliminates years of accumulated workarounds and positions our client to onboard new hospital types without engineering rework. The transition preserved data across all four existing hospital formats without loss.
- **Expanded operational control**: Operations staff can now manage specialty and organizational configurations directly within the platform rather than maintaining them through separate processes.
###### Team augmentation in a complex project
The true complexity of a deeply integrated platform feature often reveals itself progressively, and building in the flexibility to respond is as important as the initial design choices. The phased rollout controls we put in place proved especially valuable: by allowing each organization to be configured, tested, and enabled independently, we reduced the risk of a single large transition while giving the operations team a clear, low-pressure path to production. Throughout the engagement, the ability to adapt as new requirements emerged was just as important as the original architecture.
What began as a feature enhancement evolved into a broader platform modernization effort. By replacing rigid, developer-managed configuration with a flexible operational model, the client gained a foundation that can support future growth while reducing the engineering effort required to onboard new organizations.
### Rethinking a socially-driven restaurant discovery mobile app
#### Case Study
**Sector:** Commerce & Logistics
---
###### Prioritizing people, not platform
Restaurant discovery has become increasingly algorithmic, transactional, and impersonal. Our client saw an opportunity to build something different: a mobile-first platform centered around the people using it: their experiences, preferences, social circles, and trusted recommendations.
The goal was not simply to create another map of nearby restaurants. The vision was to create a socially-driven restaurant discovery platform where users could build a living map of places they loved, places they wanted to try, and places their friends genuinely recommended.
As a greenfield initiative, the project offered significant latitude in both product direction and technical architecture. That flexibility allowed us to think beyond traditional review-platform patterns and focus on creating an experience that felt personal, collaborative, and community-driven from the ground up.
###### Building a discovery platform around social interaction
At the core of the platform was the idea that restaurant discovery is inherently social.
Users could build personalized collections of restaurants, share recommendations with friends, and discover new places through trusted social connections.
In addition to photos and written reviews, users could provide lightweight recommendation signals that encouraged participation without requiring lengthy reviews.
We also designed collaborative features that helped groups make dining decisions together, making the experience more social while reducing the friction of choosing where to eat.
The result was a discovery experience designed around real-world social behavior instead of static business listings.
###### Aggregating and normalizing large-scale location data
One of the project’s largest technical challenges was creating a robust and scalable restaurant data pipeline.
The application relied heavily on external location and business intelligence providers, including:
- Google Places API
- Yelp APIs
- Foursquare’s open-source POI dataset
Each platform exposed different strengths, limitations, data structures, identifiers, rate limits, and coverage gaps. Creating a cohesive and performant discovery experience required significant engineering to unify information from multiple providers into a consistent, reliable user experience.
We designed the platform to intelligently combine data sources while minimizing duplication and inconsistencies between providers. The platform was designed to manage large volumes of location and business data while maintaining accuracy, responsiveness, and a consistent user experience.
Because restaurant discovery is highly dependent on responsiveness and perceived freshness, performance became a first-class architectural concern early in development.
###### React Native and a mobile-first architecture
The application was built in React Native to support cross-platform mobile development while maintaining a highly interactive and native-feeling user experience.
Given the social and map-centric nature of the platform, the application required careful coordination between:
- Real-time location services
- Mobile map rendering
- External API communication
- Media uploads
- Social interactions
- High-frequency UI state updates
The architecture emphasized flexibility and rapid iteration, allowing product features and experimentation to evolve quickly as the platform vision matured.
Because the product was built from scratch, we established a flexible technical foundation that supported rapid iteration, scalability, and long-term growth.
###### Turning restaurant discovery into a shared experience
The most important outcome of the project was not simply the successful launch of a restaurant application; it was the creation of a product experience intentionally designed around trust, relationships, and participation.
###### Technology should amplify human recommendations
This project reinforced an idea we believe strongly in: the best discovery platforms do not replace human recommendations; they amplify them.
Technology is exceptionally good at aggregating information, mapping locations, and scaling access to data. But people still trust people. The most valuable recommendations are often contextual, emotional, and social.
By building a flexible mobile platform that blended location intelligence with real social interaction, we helped position our client to compete in an extremely crowded market with a product experience focused on authenticity and user engagement rather than pure directory scale.
The result was a highly extensible foundation for continued innovation in restaurant discovery, social recommendations, and location-based experiences.
### Building a cultural geospatial mobile app
#### Case Study
**Sector:** Commerce & Logistics
---
###### Creating a contextual educational experience
Our client operates in the cultural education space and wanted to create a modern, mobile-first experience that helped users explore and engage with points of interest in Massachusetts.
The goal was bigger than simply displaying locations on a map. The application needed to create a contextual, location-aware educational experience that encouraged discovery in real time, whether users were actively traveling, attending events, or simply exploring their local communities.
The platform also needed to support rich educational content, evolving event data, and long-term scalability across both iOS and Android devices.
###### Building a location-aware educational platform
We designed and developed a cross-platform mobile application using React Native that allows users to discover and learn about historical and cultural points of interest throughout the region.
The application combines interactive mapping, background location awareness, and rich media experiences to create a highly immersive exploration platform.
Users can:
- Discover nearby points of interest in real time
- Learn about locations through curated educational content
- Browse rich media including photography and supporting materials
- Discover events happening at or near participating locations
- Continue exploring seamlessly while traveling throughout the region
A key focus of the project was ensuring the experience felt responsive and useful even while users were actively moving between locations.
###### Performance at scale
Because the application relies heavily on geospatial data, media assets, and dynamic event information, performance and data efficiency became critical architectural concerns early in development.
We implemented a series of custom caching and data-loading strategies designed to:
- Reduce unnecessary network requests
- Improve responsiveness while traveling between areas
- Minimize mobile bandwidth usage
- Ensure fast access to nearby locations and media
- Support reliable operation in areas with inconsistent connectivity
These optimizations allowed the application to maintain a smooth user experience even while handling large sets of location-based content.
###### A unified content management system
In addition to the mobile application itself, we developed a web-based administrative platform that allows the client to manage the entire content ecosystem behind the app.
Administrators can:
- Create and update points of interest
- Manage educational content and media assets
- Publish and update event information
- Maintain content without requiring mobile app updates
This centralized management approach gave the organization the flexibility to continuously expand and evolve the platform over time.
###### Connecting education with exploration
The final platform created a bridge between cultural education and real-world exploration.
Instead of requiring users to seek out information manually, the application surfaces relevant locations and experiences contextually based on where users are and what is happening nearby.
This transformed the experience from a static educational directory into an active discovery platform that encourages deeper engagement with local history, culture, and events.
###### Technology supporting real-world experiences
This project reflects our approach to building software that balances performance, usability, and long-term maintainability.
By combining React Native mobile development, geospatial awareness, scalable content management, and thoughtful performance optimization, we delivered a platform capable of supporting rich educational experiences across an entire region while remaining flexible enough to grow alongside the organization’s mission.
### Custom Laravel package development
*Published on December 1, 2023*
---
#### **Getting Started - autoload your namespace**
If you don’t already have a Laravel project up and running, go ahead and create a fresh install of Laravel so you can build the package there.
The first real step in creating a Laravel package is to pick a namespace. Typically, this will be the camelcase name of a GitHub repository that you intend to use for distributing your package followed by a slash and the name of your package. For instance, our package is residing in the repository, so the camelcase equivalent is Kirschbaum/LaravelSparkPages.
Why bother with picking a namespace first? You’re going to need it for the following reasons:
1. **The namespace in your composer.json file enables your package to be loaded while you’re developing it.** You must do this manually because you can’t “compose require” your package yet, which normally would autoload your package for you. In fact, go ahead and include your package manually by adding the line below to your psr–4 block in composer.json:
```
"psr-4": {
"Kirschbaum\\LaravelSparkPages\\": "./packages/kirschbaum/laravel-spark-pages/src"
}
```
2. **You need your namespace to create your directory structure.** Go ahead and create a folder starting at the root level so that you will have the equivalent of “packages/kirschbaum/laravel-spark-pages/src” (adjusted for your namespace). The reason why you shouldn’t include your work within an existing package dependency folder (like “vendor”, for instance) is because if you ran composer update, your work might get deleted.
3. **You need to put your namespace at the top of most of the files within your package.** For instance, the key file that loads your app is going to be a service provider that needs to have “\[namespace\]Kirschbaum\\LaravelSparkPages;” at the top of it. If your package includes any controllers or models, they’re also going to have to have the same namespace at the top of each of them.
4. **When you create your package's composer.json file (more on this further below), you will be able to use this namespace to autoload your package into your users' Laravel projects.**
#### **Create a service provider file in your package**
Now you can create the file you named above. It needs to be placed in your `packages/kirschbaum/laravel-spark-pages/src` folder (adjusted for your namespace), and you will also need to create a routes/web.php file for that folder as well. Go ahead and do that now. The service provider needs to be structured like so:
```
namespace Kirschbaum\LaravelSparkPages;
use Illuminate\Support\ServiceProvider;
class PackageServiceProvider extends ServiceProvider
{
public function boot()
{
}
public function register()
{
}
}
```
Note you should replace “PackageServiceProvider” with a relevant name for your app. Run “composer dump” so your changes to composer.json will take effect and this class will be recognized by Laravel.
Any routes your package may have should go in the boot() method, like so:
```
$this->loadRoutesFrom(__DIR__.'/../routes/web.php');
```
Any views your package will need to be registered in, fittingly, the register() method. You can do that like so:
```
$this->loadViewsFrom(__DIR__.'/../resources/views', 'laravel-spark-pages');
```
Notice for the last file, you are planning to load your package’s views from your main Laravel app in a folder named after your namespace that doesn’t exist yet (laravel-spark-pages). We’ll cover that in the below section “Copy migrations and customizable files to the Laravel app.”
#### **Create a composer.json file in your package**
It needs to be placed in your “packages/kirschbaum/laravel-spark-pages/” folder (adjusted for your namespace) and can follow this general structure:
```
{
"name": "kirschbaum/laravel-spark-pages",
"description": "Easy CMS-like page creation and editing for Laravel Spark",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Bryan Miller",
"email": "bryan@kirschbaumdevelopment.com"
},
{
"name": "Nathan Kirschbaum",
"email": "nathan@nathankirschbaum.com"
}
],
"keywords": ["laravel-spark", "content-management", "pages"],
"require": {},
"autoload": {
"psr-4": {
"Kirschbaum\\LaravelSparkPages\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Kirschbaum\\LaravelSparkPages\\PackageServiceProvider"
]
}
}
}
```
Some of the above lines are optional, but the key line to include (adjusted for your namespace) is
```
"Kirschbaum\\LaravelSparkPages\\": "src"
```
#### **Add the folders you’ll need**
At this point, you can start to think of your package a little bit like a mini-Laravel app in terms of structure and how you’re going to add functionality. Put your Controllers, Models, and routes/web.php file into the “/src” folder alongside your service provider. Note that any routes you define in your routes/web.php folder will have to include the whole path of your package’s namespace when referencing controllers, such as this:
```
Route::get('/pages/{slug}/edit', '\Kirschbaum\LaravelSparkPages\PageController@edit');
```
Add a few more sibling folders to your “/src” folder as needed, such as “database”, and "resources" folders. The "resources" folder can be structured like it is in Laravel, with resources/view, resources/js, resources/css. In your database folder, you can add a migrations folder along with any migrations you might need. In your views folder, you can add blade files to your package’s controller and routes will need. If you have any JS files, put them in there, etc.
#### **Copy migrations and customizable files to the Laravel app**
You will have to publish any migrations you include in your package to the main Laravel app so they’ll get picked up when php artisan migrate gets run. You can do this by including the below block of code in your package’s service provider’s boot() method:
```
$this->publishes([
__DIR__.'/../database/migrations/' => database_path('migrations')
], 'migrations');
```
and then by running the below command from the terminal (adjusted for your namespace):
```
php artisan vendor:publish --provider="Kirschbaum\LaravelSparkPages\PackageServiceProvider" --tag='migrations'
```
Note you will still have to run “php artisan migrate” for the migration to run.
If users may need to overwrite some of your package's default configurations, you can create a configuration file to get published in your users' config directory in a similar manner:
```
$this->publishes([
__DIR__.'/../config/your-config-filename.php' => config_path('your-config-filename.php')
], 'config');
```
and again by running the command from the terminal (adjusted for your namespace):
```
php artisan vendor:publish --provider="Kirschbaum\LaravelSparkPages\PackageServiceProvider" --tag='config'
```
If there are any files in your package that users will likely want to customize, such as Blade, CSS or JS files, then include php artisan publish commands so that these files can be referenced outside of your package. They could be grouped together (as below) or separated out into individual calls to the publishes() method for more granular control. This way, if users ever want to update your package, all of their customized changes won’t get deleted as they can update their own copies of your package's files. You can do this by adding the below block to the same boot() method as mentioned above:
```
$this->publishes([
__DIR__.'/../views/' => base_path('resources/views/vendor/laravel-spark-pages'),
__DIR__.'/../js/' => base_path('resources/assets/js/vendor/laravel-spark-pages')
], 'assets');
```
and then by running the below command from the terminal (adjusted for your namespace):
```
php artisan vendor:publish --provider="Kirschbaum\LaravelSparkPages\PackageServiceProvider" --tag='assets'
```
And you’re ready to go!
### Laravel Translations Loader
#### Using Laravel translations in Javascript.
*Published on December 2, 2023*
---
Have you ever wanted to use the same Laravel translations you use on the back-end in your front-end code?
Laravel Translations Loader is a webpack loader that enables you to load your Laravel translations into your javascript bundle.
Instead of doing HTTP requests to fetch translations, the package enters in the middle of the asset compilation process and translates your PHP translation files (JSON and PHP) into a JSON object so you can use however you like.
It works out of the box with packages like vue-i18n or with a few configuration tweaks with the popular i18next package.
#### **Show me the code**
The first step, is to install the package using NPM or Yarn.
```
npm install @kirschbaum-development/laravel-translations-loader --save
yarn add @kirschbaum-development/laravel-translations-loader --save
```
Basically, in your javascript file, you just need to include the following line to import your language bundle.
```
import languageBundle from
'@kirschbaum-development/laravel-translations-loader!@kirschbaum-development/laravel-translations-loader';
```
This will load and parse all your language files, including PHP and JSON translations. The `languageBundle` will look something like this:
```
{
"en": {
"auth": {
"failed": "These credentials do not match our records."
}
},
"es": {
"auth": {
"failed": "Estas credenciales no coinciden con nuestros registros."
}
}
}
```
Along with all other translations you may have on your translations folder.
There are options for loading either just PHP or JSON translations files, as well as adding a namespace between the lang keys and the actual translations that some packages require. You can check the different loading options on the readme of the project. And please fill out an issue if you have any troubles or suggestions.
#### **Example using vue–i18n**
Notice you can directly pass the `languageBundle` object as a parameter into the `VueI18n` constructor.
```
import languageBundle from '@kirschbaum-development/laravel-translations-loader!@kirschbaum-development/laravel-translations-loader';
import VueI18n from 'vue-i18n';
Vue.use(VueI18n);
const i18n = new VueI18n({
locale: window.Locale,
messages: languageBundle,
})
```
And on any vue component, you can just use the `$t` function, like the following example:
```
{{ $t('auth.failed') }}
```
### Nova Inline Select
#### An inline select field for Laravel Nova.
*Published on December 3, 2023*
---
Laravel Nova is a fantastic tool that we at Kirschbaum Development have been using for developing both client projects and internal ones.
We have some internal resources that we manage with a small Laravel Nova project. Managing the resources was quite easy and has saved us a lot of manual work. There was however one small tool we kept wanting to reach for, but unfortunately it didn’t exist.
To update the status of a resource we would have to go to the update view, change the status, then save it. This became more cumbersome when more that one resource needed the status updated.
So we built Nova Inline Select.
This great little tool allows us to change the status of the resource easily without having to edit. It can be used directly from both the index and detail views, saving a lot of time.
To start using it, install through Composer:
```
composer require kirschbaum-development/nova-inline-select
```
It works the same as Nova’s `Select` field with some added sugar. Read the full documentation over at Github and grab a copy!
### Laravel Github Actions
#### CI/CD using Github Actions for Laravel.
*Published on December 4, 2023*
---
This blog post will show you how to configure Github actions to run your `phpunit` tests, how to deploy after your tests pass and a few other things. We'll also show you how to connect to a database (MySQL, Postgres or SQLite) to run your test suite.
The first thing we'll need is a docker container with PHP installed that is able to run our Laravel test suite. We, at KDG, put together a [docker container](https://cloud.docker.com/u/kirschbaumdevelopment/repository/docker/kirschbaumdevelopment/laravel-test-runner) specifically for this purpose. The docker container can be found at:
- PHP 8.1: `kirschbaumdevelopment/laravel-test-runner:8.1`
- PHP 8.0: `kirschbaumdevelopment/laravel-test-runner:8.0`
- PHP 7.4: `kirschbaumdevelopment/laravel-test-runner:7.4`
- PHP 7.3: `kirschbaumdevelopment/laravel-test-runner:7.3`
- PHP 7.2: `kirschbaumdevelopment/laravel-test-runner:7.2`
The Github repository can be found at [Laravel Test Runner Container.](https://github.com/kirschbaum-development/laravel-test-runner-container) Please open issues or send pull requests if you find any libraries missing for your needs.
We also created this [example repository](https://github.com/luisdalmolin/laravel-ci-test) which is using the setup mentioned below to run the test suite.
Alright, let's get into it!
#### **Setting up the Github Action**
You may need to tweak a few things, but basically you should be able to just copy and paste the following configuration to your Github actions.
`.github/workflows/ci.yml`
```
on: push
name: CI
jobs:
phpunit:
runs-on: ubuntu-latest
container:
image: kirschbaumdevelopment/laravel-test-runner:7.3
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test
ports:
- 33306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Install composer dependencies
run: |
composer install --no-scripts
- name: Prepare Laravel Application
run: |
cp .env.ci .env
php artisan key:generate
- name: Run Testsuite
run: vendor/bin/phpunit tests/
```
*Don’t forget to configure your env!*
On this example, I created a `.env.ci` file with some of the configurations. Here is what is important for you to configure in this file:
```
# database
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=test
DB_USERNAME=root
DB_PASSWORD=password
```
You may need a few tweaks for your own configuration but afterwards you should be able to see the build passing.
#### **Using PostgreSQL or SQLite instead of MySQL**
To use PostgreSQL instead of MySQL, you can easily change the `services` section in your CI config with the following:
```
services:
postgres:
image: postgres:10.8
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
```
And also change your `.env.ci` DB configuration to:
```
DB_CONNECTION=pgsql
DB_HOST=postgres
DB_PORT=5432
DB_DATABASE=test
DB_USERNAME=postgres
DB_PASSWORD=postgres
```
And to use *SQLite*, you should be able to just remove the `services` section entirely, and change your environment configuration to:
```
DB_CONNECTION=sqlite
DB_DATABASE=:memory:
```
#### **Compiling assets is easy too**
If you use the `kirschbaumdevelopment/laravel-test-runner` docker container to run your suite then it already has Node/NPM/Yarn installed. You can install dependencies/compile assets by simply adding a new step into your pipeline:
```
- name: Install front-end dependencies
run: |
npm install
npm run dev
```
And that should be enough!
#### **Deploying your code after your test suite passes**
You can easily automatically deploy your code ONLY if all of your tests are passing. I’m going to assume you already have an automated way to deploy your code here and will not go into how to do this or all the different options available.
Let’s say you want to deploy to Laravel Forge after your build passes.
```
- name: Deploy to Laravel Forge
run: curl ${{ secrets.FORGE_DEPLOYMENT_WEBHOOK }}
```
In this case, you need to register `FORGE_DEPLOYMENT_WEBHOOK` in the repository secrets.
Or, if you want to deploy to Vapor:
```
- name: Deploy to Laravel Forge
run: |
export VAPOR_API_TOKEN="${{ secrets.VAPOR_API_TOKEN }}"
vapor deploy staging
```
And of course, register `VAPOR_API_TOKEN` in your repository secrets.
#### **Badges**
Github recently implemented the ability to include badges with the last status of your actions. You probably saw some of these around open source projects in the past. If you want to include in your project, you can find the documentation [here](https://help.github.com/en/github/automating-your-workflow-with-github-actions/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository). But in short, the only thing you need is the following markdown:
```
[](https://github.com/{owner}/{repo}/actions)
```
Owner is the owner of the repo, repo is obviously the repo name, and `workflow_name` is the `name` property in your workflow file (usually line 2).
Below you can see the rendered badge from the example repo I created:
The code for this badge looks like this:
```
[](https://github.com/luisdalmolin/laravel-ci-test/actions)
```
#### **Extra: Configuring Laravel Nova on your pipeline**
If your Laravel project uses Laravel Nova, you will need to authenticate composer before installing dependencies. You can configure Nova authentication by adding the following step:
```
- name: Configure composer for Laravel Nova
run:|
composer config "http-basic.nova.laravel.com" "${{ secrets.NOVA_USERNAME }}" "${{ secrets.NOVA_PASSWORD }}"
```
Also, don’t forget to add `NOVA_USERNAME` and `NOVA_PASSWORD` to your Github Actions secrets. This configuration can be found the repository settings > Actions.
### Mail intercept
#### Conduct better testing with Mail Intercept.
*Published on December 5, 2023*
---
Writing tests can be hard. Should I mock? Perhaps I should spy? Or maybe there is a built in `fake()` for this?
Writing tests for mail can be even harder. Enter Laravel’s Mail Fake. Use that at the beginning of your test, do your heavy lifting, then run some assertions against the fake. It is extremely helpful to verify that mail gets sent out and peek at the mail to ensure it got sent to the right person. As a sidenote, a little known plus with faking mail is that it will disable mail from sending accidentally during tests. You never want to be the one who sent out a few hundred emails accidentally… but back to the subject at hand.
But faking mail can only take you so far. How do you test the email body contains the correct string? What about that custom header? Or even the subject? Those kinds of details need to be tested, but can’t be faked. Right Harry?
Say hello to Mail Intercept!
Mail Intercept for Laravel is a new way of testing mail by intercepting, not faking, email so we can dissect it, turn it upside down, and inspect everything.
Under the hood, it is quite simple in that it forces the mail driver to be an array pushing all those emails into memory. We then grab those emails and run assertions on them! That’s it, pretty simple really!
But we didn’t want to stop there. We wanted it to be really easy to run assertions against those emails to check for specific things. Here are the assertions available to you as of this writing:
```
$this->assertMailSentTo($to, $mail);
$this->assertMailNotSentTo($to, $mail);
$this->assertMailSentFrom($from, $mail);
$this->assertMailNotSentFrom($from, $mail);
$this->assertMailSubject($subject, $mail);
$this->assertMailNotSubject($subject, $mail);
$this->assertMailBodyContainsString($content, $mail);
$this->assertMailBodyNotContainsString($content, $mail);
$this->assertMailHasHeader($header, $mail);
$this->assertMailMissingHeader($header, $mail);
$this->assertMailHeaderIs($header, $value, $mail);
$this->assertMailHeaderIsNot($header, $value, $mail);
```
You will notice that each assertion accepts, as the last parameter, a `$mail` object. Where does that come from you might ask? Let’s look at a simple test to answer that question.
```
namespace Tests;
use App\Jobs\SomeJobThatSendsMail;
use Illuminate\Foundation\Testing\WithFaker;
use KirschbaumDevelopment\MailIntercept\WithMailInterceptor;
class MailTest extends TestCase
{
use WithFaker;
use WithMailInterceptor;
public function testMail()
{
$this->interceptMail();
$email = $this->faker->email;
SomeJobThatSendsMail::dispatchNow($email);
$interceptedMail = $this->interceptedMail()->first();
$this->assertMailSentTo($email, $interceptedMail);
}
}
```
The first thing to notice is the use of the `WithMailInterceptor` trait. This pulls in the ability to intercept and makes available our mail assertions.
Just like faking mail, we must call the `$this->interceptMail()` before mail gets sent.
After the mail is sent we need to retrieve the freshly sent mail. We grab these with `$this->interceptedMail()`. This will return a collection of sent mail. And since it is a collection, we can use the `first()` method to grab the only email we sent.
Now we can run all the assertions against that mail object. If instead we need to run assertions against multple emails, we can use the `each()` method to loop through them.
We’ve included a good starting point for assertions. However, if there aren’t enough for your liking, the mail object is an just an instance of `Swift_Message` with all of its available methods. Head over to the Swift Mailer Docs for all available methods.
Interested in using this test suite in your project now? Head over to Mail Intercept on Github, pull it into your project with Composer and start intercepting and testing mail with confidence!
### Eloquent power joins with Laravel
#### Add some Laravel magic to your Eloquent joins.
*Published on December 6, 2023*
---
If you have some experience using databases, it is very likely you have used `joins` at least once in your career. Joins can be used for a bunch of different reasons, from selecting data from other tables to limiting the matches of your query.
I'm going to give a few examples on this post, so, in order to contextualize the examples, imagine we have the following database/models structure.
`User` -> `hasMany` -> `Post`
`Post` -> `hasMany` -> `Comment`
`Post` -> `morphMany` -> `Image`
On Laravel, using eloquent, joining the `posts` table would look something like this:
```
User::select('users.*')->join('posts', 'posts.user_id', '=', 'users.id');
```
In case you want to join the posts and the comments table, your query would look something like this:
```
User::select('users.*')
->join('posts', 'posts.user_id', '=', 'users.id')
->join('comments', 'comments.post_id', '=', 'posts.id');
```
This is fine and we can understand, but we can do better. We already have all these relationships defined in our models, but we are repeating some of the implementation details when we write the join statements. So, instead of doing this, wouldn't be cool if you could just do the following?
```
// example 1
User::joinRelationship('posts');
// example 2
User::joinRelationship('posts.comments');
```
This is less code to read, and more importantly, easier code to read. It also hides any implementation details on how your relationships work. So, if your relationship changes, your joins will be automatically updated.
#### **Introducing the Eloquent Power Joins package**
We felt the way we did joins in our applications wasn't really the “Laravel way”, so we decided to introduce some of the Laravel fine touch into the way we do joins.
`joinRelationship` is a method introduced by the [Eloquent Power Joins package](https://github.com/kirschbaum-development/eloquent-power-joins). It works with any type of the existing Laravel relationships.
The installation of the package is as simple as just running the following composer command, and you should already have access to everything that will be mentioned on this post.
```
composer require kirschbaum-development/eloquent-joins-with-extra-powers
```
**Joining polymorphic relationships**
The `joinRelationship` method also works polymorphic relationships. Besides performing the regular join, it also performs the `{morph}_type == Model::class` check, as you can see below.
```
Post::joinRelationship('images')->toSql();
// select * from posts
// inner join images on images.imageable_id = posts.id AND images.imageable_id = 'App\\Post'
```
**Joining nested relationships**
And, it also works with nested relationships.
```
User::joinRelationship('posts.images')->toSql();
// select * from users
// inner join posts on posts.user_id = users.id
// inner join images on images.imageable_id = posts.id AND images.imageable_id = 'App\\Post'
```
**It works with any relationship**
The package will work with any of the native relationship types provided from Laravel.
`BelongsToMany` will make 2 joins considering the pivot table as well. `HasManyThrough` also makes the 2 necessary joins.
Eloquent Power Joins also applies any soft deletes clauses in case the related model uses the `SoftDeletes` trait.
But, the package also provides you with a few other very useful features, as you can see below.
**Applying extra conditions to the joins**
You can apply any extra condition you need to the joins, as well.
```
User::joinRelationship('posts', function ($join) {
$join->where('posts.published', true);
});
```
For nested calls, and/or `BelongsToMany` or `HasManyThrough` relationships, you need to pass an array with the relationship as the key.
```
User::joinRelationship('posts.images', [
'posts' => function ($join) {
$join->where('posts.published', true);
},
'images' => function ($join) {
$join->where('images.cover', true);
},
]);
```
**Using model scopes inside the callbacks 🤯**
We consider this one of the most useful features of this package. Let's say, you have a `published` scope on your `Post` model:
```
public function scopePublished($query)
{
$query->where('published', true);
}
```
When joining relationships, you *can* use the scopes defined in the model being joined. How cool is this?
```
User::joinRelationshio('posts', function ($join) {
// the $join instance here can access any of the scopes defined in the Post model 🤯
$join->published();
});
```
#### **Querying relationship existence**
[Querying relationship existence](https://laravel.com/docs/7.x/eloquent-relationships#querying-relationship-existence) is a very powerful and convenient feature of Eloquent. However, it uses the `where exists` syntax which is not always the best and more performant choice, depending on how many records you have or the structure of your table.
This package also implements almost all Laravel methods for querying relationship existence using `joins` instead of `where exists`.
##### **Performance**
First thing to be aware here, is that the below example is one use-case where using joins over where exists is a lot more performant. You shouldn't assume this is true for every query, and you should use tools like [Laravel Debugbar ](https://github.com/barryvdh/laravel-debugbar), [Laravel Telescope](https://laravel.com/docs/7.x/telescope) or any tool of your choice to figure out what's best for *YOUR* use-case.
That said, below you can see one example of the MySQL CPU usage after deploying a change to use hasUsingJoins instead of has, in one of our client's application. MySQL was running on RDS, and this image was took from AWS CloudWatch.
##### **Show me the code**
Below, you can see the methods this package implements and also the Laravel equivalent.
```
User::has('posts');
User::has('posts.comments');
User::has('posts', '>', 3);
User::whereHas('posts', function ($query) {
$query->where('posts.published', true);
});
User::doesntHave('posts');
```
**Package implementations using joins**
```
User::hasUsingJoins('posts');
User::hasUsingJoins('posts.comments');
User::hasUsingJoins('posts.comments', '>', 3);
User::whereHasUsingJoins('posts', function ($query) {
$query->where('posts.published', true);
});
User::doesntHaveUsingJoins('posts');
```
#### **Sorting your query results**
Another useful feature os to sort your query results using a column from another table using the `orderByUsingJoins` method.
```
User::orderByUsingJoins('profile.city')->toSql();
// select "users".* from "users"
// inner join "user_profiles" on "user_profiles"."user_id" = "users"."id"
// order by "user_profiles"."city" asc
```
You can also sort your results by aggregations (`COUNT`, `SUM`, `AVG`, `MIN` or `MAX`).
For instance, to sort users with the highest number of posts, you would do this:
```
$users = User::orderByCountUsingJoins('posts.id', 'desc')->get();
```
Or, to get the list of posts sorted by the ones with comments which contain the highest average of votes.
```
$posts = Post::orderByAvgUsingJoins('comments.votes', 'desc')->get();
```
And you also have methods for `SUM`, `MIN` and `MAX`:
```
Post::orderBySumUsingJoins('…');
Post::orderByMinUsingJoins('…');
Post::orderByMaxUsingJoins('…');
```
#### **Joins, the Laravel way**
IMO, one of the advantages of the package is being able to write code in a more “Laravel way”. So, below you can see a few examples of how much better the code looks after using it. Any examples described here produces the EXACT same result.
###### **Example 1**
```
BuilderFile::select('builder_detail_builder_file.*')
->join('builder_detail_builder_file', 'builder_files.id', '=', 'builder_detail_builder_file.builder_file_id')
->join('builder_details', 'builder_details.id', '=', 'builder_detail_builder_file.builder_detail_id')
->join('documents', 'builder_details.document_id', '=', 'ces_documents.id');
```
**With Eloquent Power Joins**
```
BuilderFile::joinRelationship('details.document');
```
###### **Example 2**
```
Document::query()
->join('term_relations', function ($join) {
$join
->on('term_relations.relationable_id', '=', 'ces_documents.id')
->where('term_relations.relationable_type', '=', Document::class);
})
->join('terms', 'term_relations.term_id', '=', 'terms.id')
->join('vocabularies', 'terms.vocabulary_id', '=', 'vocabularies.id')
->get();
```
**With Eloquent Power Joins**
```
Document::query()
->joinRelationship('related.terms')
->joinRelationship('related.vocabulary')
->get();
```
That's it. Hopefully this package is going to be as useful to you as it is to us. Happy joining!
### How Tailwind CSS adds value in web development
*Published on December 8, 2023*
---
#### **Tailwind delivers branded, polished UI with less development time**
Tailwind is a utility-first CSS framework. It follows the path of projects like Beard CSS and Tachyons CSS, and brings more focus to the developer experience and delivering designs for the web efficiently. Implementing a company’s brand is easy with Tailwind.
Tailwind CSS class names are design constraints that don't represent a particular component name or identity. This is what makes Tailwind designs look “clean.” Spacing, font sizes, colors, and other design elements are consistent because they are constrained.
#### **How it’s different from other CSS frameworks**
Most CSS frameworks offer a set of utility classes to customize components, especially when it comes to spacing. For example, Bootstrap includes utility helpers that enable you to write mx-auto or mx-5 (and so on) to control spacing in a more granular way. You use them alongside the typical Bootstrap classes like btn or card, but the utility aspect is secondary in Bootstrap. It’s an opinionated, pre designed CSS library. While it offers customization options, making Bootstrap not look like Bootstrap is quite a task, as is maintaining your customization. The same is true for similar libraries such as Foundation, Bulma, and Skeleton.
You can think of these traditional frameworks as templating frameworks. Tailwind is different. It’s a design framework for the web. It’s extremely efficient for developing custom designs and unique visual identities. Tailwind does a great job not getting in your way, behaves predictably, compiles to a small size in production, and has clear documentation. It isn’t opinionated in any way, and comes with a common-sense preset config that’s easy to make yours.
Tailwind also doesn’t assume which browsers you’ll support on your project. On most projects, you can get away with writing very little custom CSS. It offers first-class support for responsive design, modern CSS features like Flexbox and CSS Grid, transitions, and accessibility helpers; and it’s easy to extend. You can customize it by altering its configuration, allowing you to make a customized (and portable) design system that can be moved from project to project.
#### **Why do some developers hesitate to use Tailwind?**
Some developers against using Tailwind argue it’s a more verbose markup that feels like inline styling, isn’t reusable, and lacks common pre-designed components to start with.
#### **Pushback: Verbose markup that feels like inline styling**
There’s no way around it. It isn’t the cleanest looking markup.
Here’s a button in Tailwind:
```
```
Compare that to a button authored in a more traditional way:
```
```
However, there’s more to the story. The second example omits the CSS code required to make it happen. Here’s the other part you’d have to write somewhere:
```
.button .button-primary {
background: #5850ec;
border: 1px transparent;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25rem;
padding: 1rem 0.5rem 1rem 0.5rem;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
color: white;
transition-property: background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: 0.15s;
}
```
So yes, with Tailwind, HTML markup is verbose. But without it you’re not writing less, you’re writing the code elsewhere and hiding it from the markup.
If you look at the minified CSS source in the browser it will look “uglier” than the Tailwind markup in the HTML source. The Tailwind skeptic asks “Why would I look at the CSS source?” to which I ask “Well, why are you looking at the HTML source?” There’s a big advantage to the Tailwind approach. While developing you can “design in the browser” with immediate feedback and without the need to compile anything. This approach supercharges the UI development workflow – especially when it comes to fine tuning
While on the surface this may look like inline styles, it’s anything but. Tailwind classes are constrained. You can’t pick any size, but use classes are configured according to the design system or constraints you have in place. Spacing isn’t just consistent on an element basis, it’s consistent everywhere. Colors aren’t just consistent on buttons, they’re consistent everywhere.
#### **Pushback: Reusability and Tailwind**
The skeptic will ask “What about reusability?” This leads us to what Tailwind is particularly good at: delivering projects for the modern web.
Whether working with traditional server-rendered applications or more Javascript heavy ones, duplication is best avoided by creating reusable components. CSS is only part of them. Most often components encapsulate HTML markup and any Javascript functionality in addition to CSS. In this context there isn’t really any duplication, and if anything, it streamlines your workflow.
Tailwind also provides the means to further reduce duplication via the @apply directive. It enables you to author CSS within the design constraints of your project.
```
.button {
@apply py-2 px-4 border border-transparent text-sm;
}
```
That said, if you’re reaching for @apply, maybe you should take another look at how you’re writing your components. Look for opportunities to encapsulate the CSS into the component itself.
#### **Pushback: Tailwind doesn’t come with pre-designed components.**
Yes, Tailwind doesn’t come with any pre-designed components or elements. There aren’t any pre-styled buttons or forms. However, if you need some basic elements to start with, there are ways to kick start your design by pulling in the [Tailwind CSS Custom Forms](https://github.com/tailwindlabs/tailwindcss-custom-forms) as well as the [Tailwind CSS Typography Plugin](https://github.com/tailwindlabs/tailwindcss-typography), which are both official Tailwind CSS projects. There are also plenty of examples available in the Tailwind Documentation to get you started. In addition, there’s Tailwind UI, which while not free, provides plenty of well-thought, beautifully designed components.
There are resources out there to help scaffold things quickly. The lack of pre-built components in Tailwind prevents it from having a “Tailwind look”. It isn’t easy to recognize whether a project is using Tailwind just by looking at a website. That’s a good thing.
#### **Pushback: Not everyone thinks or works the same.**
It’s common in the developer community to resist a constrained approach. We tend to be attached to specific ways of working, and some of us make a living based on a specific niche. An advanced understanding of CSS is valuable, though not as necessary with Tailwind. There are always trade-offs, but you should only need to go outside the framework for odd-edge cases. Developers early in their careers tend to create complex solutions to problems we don’t really have; it’s part of the learning process. On the other end of their careers, developers with years of experience doing things in a specific way are often not open to new ways, and sometimes sell complex solutions to problems that are simple today.
Every project is different, and Tailwind may not be the best solution to a specific problem; there’s no such thing as a silver bullet in web development. But Tailwind often enables you to deliver more value efficiently, and it’s rare to find a business with unlimited resources.
#### **A common language to visual design**
What makes Tailwind a convincing choice is its use as a common language across people on a project. Select any style guide or design system, along with any framework, and Tailwind. You’ll find most of the style constraints match options in your Tailwind config file. It enables you to work efficiently with the marketing and design teams at established companies. It also enables you to craft custom, polished visual identities for new companies. Having a consistent brand identity is crucial for brand recognition and being able to implement that reliably in web projects is key.
Currently, nothing else is as efficient for getting this done, and getting it done well. Tailwind was developed with user feedback (down to naming individual classes) so it isn’t surprising it reads well. You don’t need to translate human conversations to code; you just add what was asked, where it was asked, and it will do what you expect it to do. More importantly, it will do what the designer or business expects it to.
Making your CSS as polished as the style guide doesn’t require effort because its configuration *is* the style guide. You can certainly design things that don’t look right in Tailwind, but it’s much harder to do so. It enables visual consistency in a collaborative development setting. While managing consistency matters in any size team, the benefit of Tailwind’s efficiency grows with each additional team member.
If you’re working on a project that already has a style guide already in place, you’ll love Tailwind. Most style guides provide the requirements for typography, spacing, and color palette. You code those constraints into the Tailwind configuration in little time, and most style guides also include pre-designed components. With the constraints already in place scaffolding the reusable brand components is a breeze.
Tailwind CSS is a powerful tool in any modern web development stack. It’s no surprise it’s being adopted by startups and leading companies for mission-critical projects.
### Why we love Laravel
#### And our clients do, too.
*Published on December 7, 2023*
---
Leveraging a robust framework like Laravel enables us to provide our clients with business solutions at a pace that wasn’t possible before. We’ve been fans since we started working with it in 2013 and, having seen the value it brings to their business, our clients are fans as well. Here are the things we love about it as developers and why you’ll love it too.
#### **Efficiency**
When we explain to people what Laravel is and why developers prefer it for building new applications and maintaining existing ones, we analogize it to a modular house. The development framework gives us the structure, walls, plumbing, and wiring so we can focus on writing business logic. All the tools we need, such as user authentication, security, testing, and templating are built right into the framework along with simple ways to access them. If your development team uses the right tools, they’re more efficient – which means fewer billable hours to reach your goals and a real savings for your bottom line.
#### **Maintainability**
One of the reasons Laravel quickly took off in popularity when it came to market is that it simplified key language complexities and adherence to best practices. The developer experience became simple and elegant. When code is easy to read, it’s easier to maintain and makes it easier to onboard new developers quickly. It also makes adding new features or upgrading more straight-forward.
Laravel also excels because of its thorough documentation. Knowing how to use the tools it provides is essential to keeping your application maintainable as it grows. Imagine the frustration you feel when you try to put together a piece of furniture with a poor set of instructions. Laravel avoids this struggle by including clean documentation that keeps the code easy to understand even as the application’s complexity grows. Not only do developers love this, but it also translates into major cost savings for the business.
#### **Sustainability**
In use for almost a decade, Laravel is a mature framework. Its version upgrades are typically focused on new features to make coding even easier versus monumental structure changes that could require significant cost and time. Ensuring the stability of applications that run critical parts of your business reduces the potential for issues that could arise from frameworks that are less established and prone to frequent changes.
The strength of the community is another aspect that adds to Laravel’s stability. Thousands of developers have adopted Laravel since the framework was born, and all of us have been contributing to the growth of the community through educational courses, open source contributions, and paid packages to leverage. At Kirschbaum, we love doing our part as a contributor to help ensure its longevity.
Businesses also benefit from having access to a diverse ecosystem of high quality development companies and extremely capable developers. This is a stark comparison to other technologies and platforms where resourcing options are often limiting.
#### **Security**
In a world where we hear almost daily of data breaches, application security is one of the main concerns for developers and businesses. Laravel is widely considered to be one of the most secure frameworks available. It gives developers important tools like its authentication system, encryption algorithm, and SQL injection prevention, among others, to ensure sensitive data and critical systems remain secure.
Of course, application security is only one part of the equation for ensuring a robust security posture. Because Laravel is used by some of the biggest companies in the world, there are many core features and add-on packages that provide enterprise-grade best practices and tools at little or no cost.
#### **Conclusion**
Our clients love Laravel because it enables them to solve problems quickly and efficiently. Laravel is a powerful tool that helps modern teams build highly customized and secure solutions. It does this without compromising maintainability or future flexibility.
Having Laravel as a solid foundation to start from allows us to stay focused on what matters most to your business – delivering well-designed and stable applications that will contribute to your success for years to come.
If you’re interested in learning more about how Laravel can help bring your project to life, we’d love to speak with you.
#### **About Kirschbaum**
Kirshbaum provides software engineering solutions to solve complex business problems. We leverage innovative approaches and bleeding edge technologies to get products and services to market quickly, while anticipating the need to scale and change as business requirements evolve. The companies we work with appreciate our ability to work seamlessly with both their technical, and non-technical teams. Our developers are not only exceptional coders, but exceptional problem solvers with business sense. If there's a more efficient path to reach your goals, we'll find it.
### Leveraging language in dev culture
*Published on December 9, 2023*
---
How we use language in development-centric circles is as important as the code we design and the sprints (development iterations) we plan. Especially as many workplaces have transitioned to full-remote (temporarily or permanently), it's more important than ever to reflect on how we communicate with each other. Just by rethinking how we phrase things in writing or over calls and video chat, we can make drastic productivity improvements.
First, let’s look at the theory of why this matters. It’s called “reciprocal determinism,” coined by psychologist Albert Bandura in the 1960s 1. A very simplified version of reciprocal determinism can be described by saying your thoughts, your words, and your actions are an interdependent triad. That is, your thoughts influence your words which influence your actions which influence your thoughts; conversely your actions influence your words which influence your thoughts and so on. One of the driving mechanisms behind this is “cognitive dissonance”. When what we say doesn’t line up with what we do or what we think, we adjust some part of that triad (thoughts or actions in this case) so it lines up better in the future 2.
The good news is this gives us a spectacular hook into continuous improvement. If we aren’t getting the results we want, and it’s difficult to change our actions directly, we can tap into our language to influence it instead. Furthermore, we can do this not just for ourselves, but with others, as well. Let’s take a look at some scenarios, and see how we can make some beneficial changes.
#### **Responsibility and Ownership**
Have you ever been on a team where something fell through the cracks? What happened to let that slide, and what happened afterward? If the fallout was The Blame Game, chances are pretty good that the responsibility, or the ownership of that task was ambiguous.
There are a lot of reasons these things can be ambiguous. Sometimes, they just are. But, more often than not a few minor adjustments can help us avoid these situations. Take, for example, some of the following phrases: “I’m digging into it”, “let’s call that a spike ticket”, “I’ll get to that later,” or “it’s in progress”. Context is everything here, but if the response to “how is \[XYZ\] going” is solely one of those phrases, that task won’t be handled.
Why? Let’s put this in the model of Reciprocal Determinism. The action we want to happen is for the task to be done, so a non-dissonant thought or conversation would involve more specific and descriptive language. For example: thinking of a bug, if instead of saying “I’m digging into it” we said “I’m looking for causes of \[XYZ symptoms\] and can let you know what I find,” what do you think the outcome would be? Instead of something ambiguous, time indistinguishable, and frankly unactionable, we’ve transformed the task into clear actions we’re taking (“looking for causes of...”) with a discernible hook to allow others into the process (“...can let you know what I find”). For those on the other side of that statement, it’s the difference between trying to hop a moving freight train vs. boarding a passenger train with assigned seating.
As a scrum master, this is what I strive for the daily scrum conversations to sound like. It’s not just a status update where we list off the last ticket number we worked on, but a description of what you’re actually doing to move the team closer to the goal. Even though it’s just a quick reflection of what you did and where you’re going, the more we leave the door open for others the more we can collaborate and help each other. In the previous example, just mentioning the symptoms of the bug you’re looking into may remind someone else of a similar issue they encountered previously and it’s very likely they have some insight that can help.
Furthermore, this revised statement creates clear actions for you to follow up. You’ve already established that you’re investigating the cause of those symptoms and that you’ll be following up with whomever after that. Whether or not that’s on your task list, you’ll experience a bit of discomfort (via cognitive dissonance) if you don’t actually complete those tasks.
#### **Accountability**
Let’s try another example. Say you’re on a team with a particularly annoying or cumbersome problem that everyone seems to be stepping over or around. Assuming it’s something that truly needs to be addressed, this is kind of a problem if nobody wants to take care of it. What have you noticed about the language in these situations?
In general, I’ve found it tends to be directionless. You’ll probably hear a lot of “we’ve gotta do this”, or even more nebulously “this needs to be done”. These statements feel really safe, because there’s no accountability. Saying “this needs to be done” leaves the door wide open for anyone to do it. Your Aunt Susan from Delaware might swoop in and take care of it, who knows?
Instead, let’s be specific: “Pat, can you look into this for a while and jot down a few notes on how to handle it?” We’ve established something clear that Pat can do (“jot down a few notes…”) with a semi-definite ending (“for a while”). Note here, we deferred the actual “doing” of the task in that request. Often, the truly annoying tasks are only annoying because they aren’t (or can’t be) well defined. By specifying a more concrete action, we can side-step that issue, even if only in conversation. (Getting started on that task may be all it takes to actually figure it out!)
The same is true for our personal tasks. When asked how that annoying task is going, I can respond with “I’ll be taking a look today at how we can best handle it, and will take some notes on ideal directions”. With both of these statements, we’ve tapped into that dissonant nudge to look at it for a certain amount of time (which gives a better sense of “done”), and an expected result or outcome that we can show for our time.
#### **Collaboration**
Just like how we can nudge ourselves and others to take specific and actionable steps toward our goals, we can encourage others to join us as well. Social interactions are often centered around groupings, and we as humans love to categorize things (us vs them, you vs me, tasty vs nasty, dangerous vs. safe, etc.).
We can “add” people or groups to our group, just by including them in our language. Rather than “my work,” it’s “our work.” Especially when it comes to issues, I encourage all my teams to consider an application problem to be “everyone’s” problem. Jerry wrote that code? Who cares? It needs to be fixed! “Our application needs to be fixed” is a call to arms, whereas “Jerry’s broken pile of crap took down the app” is a fight in the parking lot. Ok, maybe it’s not that extreme, but when things are down it can certainly feel like it.
Just as we can band together to handle something, sometimes we need to do the opposite. To piggyback on the scenario in the previous section (“Accountability”), sometimes, it’s better to exclude other groups or people from our language. For example, “I am going to fix this bug” is very actionable for me and it doesn’t require anyone else. Not that we don’t want help in this, but by excluding others from the action (“I” instead of “we”), it’s clear that culpability rests with me.
One last note on collaboration, when other groups are distinct from yours and need to stay that way (e.g., developers vs. system administrators), we can still make them a part of our team by referring to them like we would extended family. “We’ll need our Sysadmin friends to help with this one” resonates very differently from “we’ll have to get the Sysadmins to do this.” Without any context, I know I’d be more excited to hear the former than the latter.
#### **Improvement**
Up until now, we’ve primarily focused on influencing actions with our language. Let’s switch gears a bit and look at influencing our thoughts. If you’ve ever used a mantra or self-affirmations, you know that words can be very powerful in shaping your thoughts. “Fake it ‘til you make it” often rings true. In the development world, though, I often see the opposite happening.
What’s your immediate thought when you hear “legacy code,” for example? If you’re like most, you probably cringed just reading the term. It can be complicated, convoluted, bug-filled, fragile, and every other horror we all have tales about. That feeling is what we want to get after and change. Imagine if working on legacy code felt more like walking into an old familiar restaurant instead of a haunted house? A lot of the difference is our mindset, and the way we talk about our apps can help.
This kind of “despair” phrasing extends to quite a few aspects of development life. Not just legacy code, but “crappy libraries” or “slow services” or “antiquated protocols”, they all prime us to think poorly of the things we may not even be familiar with. Psychological priming is the phenomenon where the mere exposure of associated stimuli can influence your thoughts, words, and behaviors 3. In a famous study on priming, experimenters had participants solve crossword puzzles, then (unbeknownst to participants) timed how long it took them to exit the room afterward. One group unknowingly solved crossword puzzles with words like “geriatric,” “elderly,” and “retirement,” and the other had random, unassociated words. The result? Those in the old age priming group exiting the room more slowly than those in the control group 4. They had been “primed” to the notion of old age and its associations and inadvertently that affected their behavior.
Applying this back to our “legacy” or “bad” code, the same principle is in effect. You may be saying that it’s all in the mindset, and you’re exactly correct. Everyone will have their own approach and their own associations, but we can use our language to influence those. The goal in a lot of these situations is to shift that mindset from dismay, dismissal, or disgust, to improvement, amelioration, and for lack of a better word, enjoyment.
For example, say we’re planning out a new feature that’s trying to bridge the gap between some brand new feature or application, and the slightly aged code (see, I didn’t call it legacy :wink: ) that currently runs in production. If we’re looking to encourage the powers that be to spend time improving the legacy code for a smooth transition (and possibly better maintainability in the future), we can try priming feelings of needing “help” (rather than “despair”). Instead of “it’ll be an absolute pain to connect this to that crusty old app,” we can try “the older app will need a bit of TLC to support this.” In the former, we’ve established that we’re expecting a guaranteed unpleasant experience, that the previous code will be difficult to work with and generally that we’re not very excited to do so. In the latter, regardless of how true the former is, we’ve noted that there is extra work needed (“a bit of TLC”), but most importantly that it is a step forward (“...to support this”).
#### **Takeaways**
We’ve covered quite a bit so far, and as you made your way through this, I hope you were able to identify with at least a few of the examples. The most important piece of this whole puzzle is to realize that our words, our thoughts, and our actions are all interrelated. We can nudge any one of them by modifying the others, but here we focused mostly on our words. Personally, I’ve found the slight distance that remote work provides is an excellent opportunity to work on this. Through chat and email especially, you have myriad opportunities to refine what you say, before sending. Over time, and as we get better at being intentional with this communication, it will become second nature. The true goal of communication is to help your conversant understand your message, not to say things your conversant will understand, so please keep in mind that all of these tactics are personal and subjective. If I say to you “banana boing boing,” and you definitely understand that I’m inviting you to get coffee next Tuesday at 2 p.m. at the cute little coffee place down the street, mission accomplished!
Here’s a quick summary of the phrases we looked at, and why we modified them:
Instead of...
Try...
Why?
“I’m digging into it”
“I’m looking at the logs today to determine potential causes and will note what I find”
Changes vague, ambiguous phrase into descriptive, actionable, and timely update.
“This needs to be done”
“Sam, can you look at this for an hour or so and point us in the right direction?”
Changes passive, unactionable, un-ownable request into specific action and expected outcome.
“Alex’s crappy code broke the feature”
“Our application is broken and needs to be fixed!”
Removes the blame-game, and encourages the entire team to step up.
“Crappy old code”
“Older code in need of help”
Rephrases a negative sinkhole into a possibility for improvement.
1 [https://en.wikipedia.org/wiki/Reciprocal\_determinism](https://en.wikipedia.org/wiki/Reciprocal_determinism)
2 [https://en.wikipedia.org/wiki/Cognitive\_dissonance](https://en.wikipedia.org/wiki/Cognitive_dissonance)
3 [https://en.wikipedia.org/wiki/Priming\_(psychology)](https://en.wikipedia.org/wiki/Priming_(psychology))
4 It should be noted that a more recent replica of this study failed to produce the same results, there have been several other variants on this experiment (such as with priming “rudeness”) that have affirmed the original results. The working theory is that individual differences in experiences with the elderly and their prior associations may affect the way primacy works in this case. Mindset and experience is powerfully influential here.
### Structuring and testing your Laravel Events and Listeners
*Published on December 10, 2023*
---
Laravel has a great [eventing system](https://laravel.com/docs/8.x/events#introduction).
It allows you to dispatch events and attach a set of event listeners to that specific event which are run automatically. They can run synchronously or asynchronously (by running in the queue).
In this article, we explore one simple yet powerful and scalable approach to setting up and testing your events and listeners.
Since everything is better with an example, picture a system that needs to perform a couple of things when a new user signs up.
This is how our controller looks , simple and clean:
```
class UsersControllers
{
public function store(UserCreateRequest $request)
{
$user = User::create($request->all());
UserCreated::dispatch($user);
}
}
```
There are a few actions we need to perform when a new user signs up. Placing them directly in the controller feels a bit weird since they are more like side effects from our main action (user registration). Also, our controller wouldn’t look that good.
This seems like a perfect case to make use of event listeners, so that’s what we’re going to do. As a bonus here, we’re respecting one of the [SOLID](https://en.wikipedia.org/wiki/SOLID) principles of software development. The **Open/Closed Principle** (OCP) states that “Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”.
```
protected $listen = [
UserCreated::class => [
SendUserCreatedNotifications::class,
CreateSalesAgentCommission::class,
SyncUserInFancyMarketingSystem::class,
],
];
```
#### **How do we test this?**
Besides having good test coverage in our system where we want to be confident when refactoring our code, we want to have tests which are easy to understand and *resilient*. We could spend hours talking about resilient tests, but for now let’s just say that no one likes having to fix a bunch of seemingly unrelated tests because you changed something. A good example might be something like:
Why is my user-creation test breaking after I modified the fancy marketing system we integrate with?
So, we’re going to start with our controller. We don’t want our controller test to break when other things change, which leads us to our testing tip number 1.
#### **Tip 1 - Event::fake() for integration tests**
Laravel gives us some **really great** tools for testing. The [mocking](https://laravel.com/docs/8.x/mocking) tools are really great, and in this case we are going to make use of `Event::fake()`.
```
class UsersControllerTest extends TestCase
{
public function test_create_user()
{
Event::fake();
$response = $this->post(route('users.store'), [...imaginary payload here])
->assertSuccessful();
$this->assertDatabaseHas('users', ['name' => 'Luis Dalmolin']);
Event::assertDispatched(UserCreated::class);
}
}
```
Our controller test is only making sure the `UserCreated` event was dispatched. It doesn’t assert that email notifications were sent, or if the commission was created. This makes our test more reliable, as it cannot break when any of those things change.
…So, are we done? To have good test coverage in this feature, we’re still missing a couple of things:
1. **Testing the functionality inside our event listener classes**
2. **Asserting that our listeners are attached to the expected events**
#### **Tip 2 - Unit test your event listeners**
In order to keep this article from getting too long, we’re going to choose one of our listeners and use it as an example, but the concepts apply to any event listener. I’m calling this a unit test here because we’ll be manually instantiating the class and performing its actions, even if it touches filesystem, databases, etc.
```
class SendUserCreatedNotificationsTest extends TestCase
{
public function test_it_send_notifications()
{
Notification::fake();
Mail::fake();
$user = User::factory()->create();
$event = new UserCreated($user);
$listener = new SendUserCreatedNotifications();
$listener->handle($event);
Notification::assertSentTo($user, WelcomeNotification::class);
Mail::assertSent(NewUserCreatedAdminNotification::class);
}
}
```
The most important part of this test is that we’re manually creating (`$listener = new SendUserCreatedNotifications()`) and running `$listener->handle($event)` our listener. We ‘re making sure this job does what it needs to do and we only care about testing if the notifications are sent. We don’t care how the user gets created (that’s why we’re using the factory) or how the event is dispatched (in this case it’s not being dispatched at all). All of these things are already tested in our controller test.
**…So, can I push my PR?**
We have pretty good test coverage here, but we can still do better.
#### **Tip 3 - Assert your event listener is attached to the event you expect**
```
class SendUserCreatedNotificationsTest extends TestCase
{
public function test_is_attached_to_event()
{
Event::fake();
Event::assertListening(
SendUserCreatedNotifications::class,
UserCreated::class
);
}
}
```
Simple as that. `Event::assertListening` is a [recent contribution](https://github.com/laravel/framework/pull/36690) I made to the Laravel framework, and it covers this missing gap between asserting the events are dispatched and unit testing your event listeners code.
It’s a simple test, but it can save you from situations like when a giant git rebase results in accidentally removing one listener while fixing the conflicts on the `EventServiceProvider`.
#### **What about Model Observers?**
You may have noticed that instead of dispatching the `UserCreated` event in our controller, we could have simply dispatched the event directly in a [model observer.](https://laravel.com/docs/8.x/eloquent#observers)
Using model observers can be really handy, but since they are global, they will run everywhere. For instance, in our code where we use our user factory, we would have to do something in order to avoid sending notifications, mocking our HTTP integration with our marketing system, and so on.
However, I still think there’s space for model observers. Since deciding when to use model observers vs. event listeners isn’t trivial, I tend to ask this question:
Is this action something that always needs to happen from the developer OR from the client perspective?
If you ask your client whether every time a user is created, it should go to the marketing system, he is going to answer **yes, 100%**. But from a developer perspective, you don’t want the user going there when you’re testing your code, for instance.
Now, if from the developer perspective you should **always** assign an UUID to the model when it gets created, then yes, model observers are the way to go.
#### **Final thoughts**
This approach may seem simple, but it can grow with your application while also maintaining good test coverage by having tests that don’t break easily. Your test suite will also run faster since you aren’t running unrelated code in your tests.
Good code is code that is easy to move around (or delete). You have total control whether to run your listeners synchronously or asynchronously.
Even if you don’t follow this, hopefully this article was helpful to you in some way.
### Laravel OpenAPI Validator
*Published on December 11, 2023*
---
Everyone loves someone who's always true to their word. When you open up a public API to the world from your application, it's equally important that you stay true to your word there, as well. What can other applications do with your API? And, how do they do it?
Chances are pretty good you're exposing an API specification, probably through documentation and/or something like an [OpenAPI spec](https://swagger.io/specification/) (if you're not, please please please drop everything you're doing and take care of that). This is a spectacular way to create a clear line of communication to your users on how you'd like them to interact. Even better yet, if you hold steadfast to your "word" (spec), you'll have a steady stream of followers pining to use your app!
If you're not already familiar with the OpenAPI 3 spec, you'll want to read up a bit on it first. You can find a great overview from Smartbear (the company behind the OpenAPI Specification now, formerly Swagger Specification) [here.](https://support.smartbear.com/swaggerhub/docs/tutorials/openapi-3-tutorial.html)
#### **Introducing Laravel OpenAPI Validator**
Take your defined OpenAPI spec and automatically test your adherence to it in your PHPUnit tests. Behind the scenes this package connects the [Laravel HTTP helpers ](https://laravel.com/docs/8.x/http-tests)to the [PHP League's OpenAPI Validator](https://github.com/thephpleague/openapi-psr7-validator). Best of all, it's (almost) plug-n-play.
Start by pulling in the package:
```
composer require kirschbaum-development/laravel-openapi-validator
```
And, in any feature/integration tests that you make an [HTTP call](https://laravel.com/docs/8.x/http-tests) to your API, simply apply the trait:
```
use Kirschbaum\OpenApiValidator\ValidatesOpenApiSpec;
class HttpTest extends TestCase
{
use ValidatesOpenApiSpec;
}
```
In most situations, that's all you need to do. The trait will tap into any HTTP calls (such as `$this->putJson(...)` or `$this->get(...)`) and hand the request and response over to the validator automatically.
What can it do?
Say you have a spec, with a few paths that looks something like this:
```
openapi: "3.0.0"
// …
paths:
/test:
get:
responses:
'200':
description: OK
/form:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
formInputInteger:
type: integer
formInputString:
type: string
required:
- formInputInteger
- formInputString
responses:
'200':
description: OK
```
In my test, I'm going to assert that my simple get endpoint returns a 200:
```
/**
* @test
*/
public function testGetEndpoint()
{
$response = $this->get('/test');
$response->assertStatus(200);
}
```
Voila! We've asserted that we get a 200! And, with the trait applied to this class, we're automatically checking adherence to our OpenAPI spec as well. In the implementation, let's break something to ensure it's working.
```
class TestController extends Controller
{
public function __invoke(Request $request)
{
// Break our original implementation
// return response()->json(status: 200);
return response()->json(status: 418); // Tea, anyone?
}
}
```
When we run our test again:
```
> ./vendor/bin/phpunit
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
.E 2 / 2 (100%)
Time: 00:00.138, Memory: 22.00 MB
There was 1 error:
1) Tests\Feature\SimpleGetTest::testBasicTest
League\OpenAPIValidation\PSR7\Exception\NoResponseCode: OpenAPI spec contains no such operation [/test,get,418]
```
Yep, sure enough, 418 was not a response we were expecting!
Ok, so it can check status codes, big whoop. That was already part of our assertions anyway! Let's try it out with something a bit more complex, like our form endpoint. Here's the spec (I've omitted any `$refs` and merged it into a single layer for easier reading):
```
openapi: "3.0.0"
// …
paths:
/test:
get:
responses:
'200':
description: OK
/form:
post:
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
formInputInteger:
type: integer
formInputString:
type: string
required:
- formInputInteger
- formInputString
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
isValid:
type: boolean
description: Is this a valid object?
howShiny:
type: integer
description: How shiny this object is.
required:
- isValid
- howShiny
```
So, a few high-level things we can expect, just from the spec:
1\. It's a `POST` request
2\. The request body is required, and has two properties, `formInputInteger` that's an integer, and `formInputString` that's a string (very descriptive names are important ;-) )
3\. The response code is 200
4\. The response body is a json object with two properties, `isValid` (bool) and `howShiny` (integer).
Here's our (simplified) test:
```
/**
* @test
*/
public function testFormPost()
{
$response = $this->postJson('/form', [
'formInputInteger' => 42,
'formInputString' => "Don't Panic"
]);
$response->assertStatus(200);
$this->assertTrue($response->json()['isValid'], true);
$this->assertEquals(10, $response->json()['howShiny']);
}
```
And a simple (naive) implementation
```
class FormController extends Controller
{
public function __invoke(Request $request)
{
// ...Validation and typing here...
if ($request->formInputInteger === 42
&& $request->formInputString === "Don't Panic") {
return response()->json([
'isValid' => true,
'howShiny' => 10,
]);
}
return response()->json([
'isValid' => false,
'howShiny' => 0,
]);
}
}
```
We run our test:
```
> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.128, Memory: 22.00 MB
OK (1 test, 3 assertions)
```
And all is well! Looking at the spec, both the parameters on the request are required, so our OpenAPI validator should let us know that *before* it hits any kind of Laravel validation. Let's try that out. Modify the test and comment out one of the post fields:
```
public function testFormPost()
{
$response = $this->postJson('/form', [
'formInputInteger' => 42,
// 'formInputString' => "Don't Panic"
]);
$response->assertStatus(200);
$this->assertTrue($response->json()['isValid'], true);
$this->assertEquals(10, $response->json()['howShiny']);
}
```
Run the tests again and see:
```
> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
{
"formInputInteger": 42
}
Keyword validation failed: Required property 'formInputString' must be present in the object
Key: formInputString
Time: 00:00.107, Memory: 22.00 MB
There was 1 failure:
1) Tests\Feature\SimpleTest::testFormPost
Body does not match schema for content-type "application/json" for Request [post /form]
[...stack trace here...]
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
```
Failure, as expected! The validator lets us know that `formInputString` was a requirement on the request, and it wasn't specified. In other words, the "Body does not match schema...for Request."
Let's uncomment that line again and modify our *response* this time, and only return that it's shiny:
```
class FormController extends Controller
{
public function __invoke(Request $request)
{
// ...Validation and typing here...
if ($request->formInputInteger === 42
&& $request->formInputString === "Don't Panic") {
return response()->json([
// 'isValid' => true,
'howShiny' => 10,
]);
}
return response()->json([
'isValid' => false,
'howShiny' => 0,
]);
}
}
```
Now we get a similar error, but on the response:
```
> ./vendor/bin/phpunit --filter testFormPost
PHPUnit 9.5.2 by Sebastian Bergmann and contributors.
F 1 / 1 (100%)
{
"howShiny": 10
}
Keyword validation failed: Required property 'isValid' must be present in the object
Key: isValid
Time: 00:00.126, Memory: 24.00 MB
There was 1 failure:
1) Tests\Feature\SimpleTest::testFormPost
Body does not match schema for content-type "application/json" for Response [post /form 200]
[...stack trace here...]
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
```
The package lets us know that our "body does not match schema...for Response". Our required property `isValid` (from the spec) *wasn't* specified in our response, so it failed our test for us.
#### **Enjoy your valid fun!**
We hope you enjoy this package, it definitely simplifies life *a lot* when you're working with an API. It gives you a nice layer of accountability that keeps you true to your word! Take a look at [the repo](https://github.com/kirschbaum-development/laravel-openapi-validator) for more usage info and let us know how it goes for you.
### Implement a custom driver for Laravel Socialite
*Published on December 12, 2023*
---
Laravel Socialite is an official Laravel package to authenticate with OAuth providers. It supports authentication with Facebook, Twitter, LinkedIn, Google, GitHub, and Bitbucket. But, what if you want to use a different driver?
In our case we want to use AWS Cognito as an authentication provider. AWS Cognito allows you to authenticate using different providers, and stores centralized user data which you can use for different applications.
So, the first thing is to install Laravel Socialite, like this:
```
composer require laravel/socialite
```
Now, we create a `CognitoProvider` class which extends from `\Socialite\Two\AbstractProvider`. We need to implement the next methods, so the driver work as expected:
```
// ...
use Laravel\Socialite\Two\AbstractProvider;
class SocialiteCognitoProvider extends AbstractProvider
{
protected function getAuthUrl($state)
{
// TODO: Implement getAuthUrl() method.
}
protected function getTokenUrl()
{
// TODO: Implement getTokenUrl() method.
}
protected function getUserByToken($token)
{
// TODO: Implement getUserByToken() method.
}
protected function mapUserToObject(array $user)
{
// TODO: Implement mapUserToObject() method.
}
}
```
From the [Laravel Socialite docs](https://laravel.com/docs/8.x/socialite#routing), we have to create a `redirect` route, which basically calls the `redirect(`) method from the selected driver, like this:
```
use Laravel\Socialite\Facades\Socialite;
Route::get('/auth/redirect', function () {
return Socialite::driver('cognito')->redirect();
});
```
This `redirect()` method calls `getAuthUrl()` method under the hood where the user is redirected to the third-party provider authentication page. So, we need to provide this url in this method. We also extract how we get the base url in a different method, as we are going to use it in different places:
```
/**
* @return string
*/
public function getCognitoUrl()
{
return config('services.cognito.base_uri') . '/oauth2';
}
/**
* @param string $state
*
* @return string
*/
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase($this->getCognitoUrl() . '/authorize', $state);
}
```
The internal `buildAuthUrlFromBase()` method builds the authentication url with all the necessary parameters.
Once the user is authenticated on the third-party provider, they are redirected to the `callback` url that we define in our application. It depends on what you want to do on this controller method, but you will probably call the `user()` socialite method, like this:
```
Route::get('/auth/callback', function () {
$user = Socialite::driver('cognito')->user();
// $user->token
});
```
When you call this method, it calls the `getTokenUrl()` method to get the access token with the given code from the callback url params. So we need to provide this url:
```
/**
* @return string
*/
protected function getTokenUrl()
{
return $this->getCognitoUrl() . '/token';
}
```
Now that we have the access token we can get the authenticated user, which we'll do in the `getUserByToken()` method. In our case, we need to do a `POST` request like this:
```
/**
* @param string $token
*
* @throws GuzzleException
*
* @return array|mixed
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->post($this->getCognitoUrl() . '/userInfo', [
'headers' => [
'cache-control' => 'no-cache',
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/x-www-form-urlencoded',
],
]);
return json_decode($response->getBody()->getContents(), true);
}
```
Finally, we get a user object from the previous method, and we need to map this object into a new `User` class. In our case, we use `Laravel\Socialite\Two\User`, and map to the `User` with `mapUserToObject()`, like this:
```
/**
* @return User
*/
protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user)->map([
'id' => $user['sub'],
'email' => $user['email'],
'username' => $user['username'],
'email_verified' => $user['email_verified'],
'family_name' => $user['family_name'],
]);
}
```
Now, in your `callback()` method, you could do something like this:
```
Route::get('/auth/callback', function () {
try {
$cognitoUser = Socialite::driver('cognito')->user();
$user = User::query()->whereEmail($cognitoUser->email)->first();
if (!$user) {
return redirect('login');
}
Auth::guard('web')->login($user);
return redirect(route('home'));
} catch (Exception $exception) {
return redirect('login');
}
});
```
Depending on the provider, you might need to add some scopes to the authentication request. The scopes are a mechanism to limit the access of an user to an application.
In AWS Cognito, there are system reserved scopes, these scopes are `openid`, `email`, `phone`, `profile`, and `aws.cognito.signin.user.admin`. To know more about these scopes, [check here](https://docs.aws.amazon.com/cognito/latest/developerguide/login-endpoint.html#get-login-request-parameters). You can also create custom scopes in Cognito, [check here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html) for more information.
In the `SocialiteCognitoProvider` class, you can define custom scopes by overriding the `$scopes` and `$scopeSeparator` internal variables like this:
```
class SocialiteCognitoProvider extends AbstractProvider
{
/**
* @var string[]
*/
protected $scopes = [
'openid',
'profile',
'aws.cognito.signin.user.admin',
];
/**
* @var string
*/
protected $scopeSeparator = ' ';
// ...
}
```
To know more about the AWS Cognito scopes, check the[ official docs here.](https://aws.amazon.com/premiumsupport/knowledge-center/cognito-custom-scopes-api-gateway)
The final class will look like this:
```
// ...
use Laravel\Socialite\Two\User;
use GuzzleHttp\Exception\GuzzleException;
use Laravel\Socialite\Two\AbstractProvider;
class SocialiteCognitoProvider extends AbstractProvider
{
/**
* @var string[]
*/
protected $scopes = [
'openid',
'profile',
'aws.cognito.signin.user.admin',
];
/**
* @var string
*/
protected $scopeSeparator = ' ';
/**
* @return string
*/
public function getCognitoUrl()
{
return config('services.cognito.base_uri') . '/oauth2';
}
/**
* @param string $state
*
* @return string
*/
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase($this->getCognitoUrl() . '/authorize', $state);
}
/**
* @return string
*/
protected function getTokenUrl()
{
return $this->getCognitoUrl() . '/token';
}
/**
* @param string $token
*
* @throws GuzzleException
*
* @return array|mixed
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->post($this->getCognitoUrl() . '/userInfo', [
'headers' => [
'cache-control' => 'no-cache',
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/x-www-form-urlencoded',
],
]);
return json_decode($response->getBody()->getContents(), true);
}
/**
* @return User
*/
protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user)->map([
'id' => $user['sub'],
'email' => $user['email'],
'username' => $user['username'],
'email_verified' => $user['email_verified'],
'family_name' => $user['family_name'],
]);
}
}
```
But, how does Socialite recognize the driver? We need to add some code in the `AppServiceProvider`:
```
// ...
use Laravel\Socialite\Contracts\Factory;
/**
* @throws BindingResolutionException
*/
public function boot()
{
$socialite = $this->app->make(Factory::class);
$socialite->extend('cognito', function () use ($socialite) {
$config = config('services.cognito');
return $socialite->buildProvider(SocialiteCognitoProvider::class, $config);
});
}
```
In the `boot` method, we register our driver in the Socialite manager, so when we call `Socialite::driver('cognito')` it instantiates our `SocialiteCognitoProvider` class.
And that’s it! This is how you implement a new custom driver for Laravel Socialite. To make your life easier, we created a small package for the custom Cognito driver, that you can [check here.](https://github.com/kirschbaum-development/laravel-socialite-cognito)
### Import Laravel Vapor DNS to Cloudflare
*Published on December 13, 2023*
---
#### **What it does**
Orrison/Cumulus is an open-source package that works with Laravel Vapor to allow the user to manage their DNS records better when using Cloudflare for DNS. When a custom domain is added in Laravel Vapor, assigned to a project environment, and deployed, Laravel Vapor will automatically set up the proper DNS records in Route 53. Laravel Vapor will then display these records via the UI or Vapor CLI tool, which you would then have to copy manually into Cloudflare.
Trying to manage DNS information from Vapor to Cloudflare without Orrison/Cumulus can open your data up to risks such as human error and wasted time since it would need to be copied over manually. In its essence, Orrison/Cumulus is an open-source tool that automatically copies the proper DNS records from Laravel Vapor to Cloudflare.
#### **How it works**
Before you can effectively use Orrison/Cumulus, you will need to have a valid Cloudflare API Access Token, the domain setup as a zone in your Cloudflare account, and a fully installed and authenticated Laravel Vapor CLI. Once this is complete, you're ready to input the Orrison/Cumulus package commands.
When obtaining the Cloudflare API Access Token, the "Edit DNS Zone" template is a perfect token template to use. You will need to set the "Zone Resources" options to either "All Zones" or the correct option for your use case.
To start using this package, you will first need to install it using Composer:
```
composer global require orrison/cumulus --with-all-dependencies
```
Once installed, the first step is to add the Cloudflare API Token. You can add the token using: Cumulus Cloudflare:login.
After adding and authenticating the Cloudflare API Token, you're ready to run the import command. For example, to import the DNS records for your domain "example.com," you would run: cumulus Cloudflare:import example.com.
Subdomains are DNS records of the root domain, so you can assign a subdomain to a project environment and import its DNS records by running the import command for the root domain. For example, if you have assigned a custom domain "sub.example.com" to a project environment in Laravel Vapor. You can import its DNS records by running: Cumulus Cloudflare:import example.com.
#### **Why use Laravel Vapor**
As a serverless deployment platform for Laravel, Vapor brings many impactful benefits such as a scaling cloud framework for your application, databases, caches, metrics, automatic asset uploading, and more. Laravel Vapor offers multiple environments, rapid rollbacks, infinite deployments, and an ever-expanding library of tools.
#### **Why use Cloudflare**
Cloudflare offers sophisticated security and performance systems for websites, APIs, and applications. Operating entirely in the cloud, Cloudflare gives you an integrated set of L3-L7 network services that are easy to configure, use, and maintain. Allowing users to lower the risk of DDoS attacks, cache static content, route through many network paths, and optimize across devices, this content delivery network or CDN offers incredible security and speed advantages. Using Cloudflare is almost necessary when using API Gateway V2 in Laravel Vapor. It's one of the best ways to add automatic HTTP to HTTPS redirects that aren't available in API Gateway V2.
#### **Conclusion**
When looking to optimize your site or application, using a combination of Laravel Vapor and Cloudflare can be a powerful way to ensure security, speed, database scaling, and in-depth analytic capabilities. That said, using the Orrison/Cumulus package ensures that these tools run together seamlessly when utilizing a custom domain or subdomains. Additional commands and info can be found in the project.
### Leveraging virtual generated columns
*Published on December 14, 2023*
---
When MySQL released native JSON column types in 5.7.8, it provided developers with an easier way to store and retrieve data for applications that used it as the storage engine. Laravel quickly supported it all the way back in 5.3 in data migrations and querying with Eloquent.
While being a convenient way to store new values without adding additional columns to your data model, one of the big drawbacks is JSON columns cannot be indexed. For smaller applications, the trade-off is minimal, but as we move into more enterprise-level applications, scaling can quickly become an issue as searching this column will require full table scans.
Luckily, there is a feature also present in MySQL starting with 5.7 called virtual generated columns. What they allow us to do is everytime the JSON column data is saved, MySQL will automatically populate a virtual column with some piece of that data based on how it is defined. The best part is, virtual columns are indexable, allowing your application to scale with ease.
Let’s take a look at how this works in Laravel using a hypothetical User data model and storing multiple phone numbers in a JSON column. We have a use case of a search page of users and we want to look them up by their mobile number.
First, we need to add the virtual column to our users table:
```
Schema::table('users', function (Blueprint $table) {
$table->string('mobile_number_virtual')
->nullable()
->virtualAs("json_unquote(json_extract(phone_numbers, '$.mobile'))");
$table->index('mobile_number_virtual');
});
```
Breaking this migration down, most of it should make sense if you work with Laravel daily. However, you may be wondering where the expression comes from in the virtualAs() method. To get that, we write an Eloquent query and output the underlying SQL statement.
```
User::where('phone_numbers->mobile', $searchTerm)->get();
// underlying query
select * from users
where json_unquote(json_extract(phone_numbers, '$.mobile'))
```
What makes getting the virtual column definition by grabbing the underlying query so important is that MySQL will automatically match the query statement to the virtual column definition and use the index you defined. The virtual column and its name can stay completely invisible to developers and all they need to do is write Eloquent queries using the JSON format they already know. How cool is that?
However, we will get a MySQL error anytime we try to create or update the user because the model adds the column to the $attributes array, which then tries to write to the column on any INSERT and UPDATE calls and is not allowed.
To get around this, we need to understand what happens in the core framework when you create or update a model. Digging into the **Illuminate\\Database\\Eloquent\\Model** class, we can see the **save()** method is called anytime these operations happen. Since all your applications models extend this class, this makes overriding it quite easy. A custom trait is preferred here as we can have any model implement it when virtual columns are needed.
Let’s define an array with the virtual columns on the model so we know what to exclude when saving:
```
/**
* The virtual generated columns on the model
*
* @var array
*/
protected $virtualColumns = [
'mobile_number_virtual',
];
```
Now, let’s create our trait and have our User model implement it:
```
namespace App\Models\Concerns;
trait HasVirtualColumns
{
public function save(array $options = [])
{
if (isset($this->virtualColumns)) {
$this->attributes = array_diff_key($this->attributes, array_flip($this->virtualColumns));
}
$return = parent::save($options);
return $return;
}
}
```
We are checking to see if the model has the virtualColumns property assigned to it and then diffing it out of the attributes array before running the parent class's **save()** method.
The final step is to leverage this trait in our User model:
```
namespace App\Models
use App\Models\Concerns\HasVirtualColumns
Class User
{
Use HasVirtualColumns;
//
}
```
Leveraging virtual columns for often used queries of JSON data opens up tremendous possibilities for ensuring your queries can scale effortlessly as your dataset grows.
### Code that can handle failure
*Published on December 15, 2023*
---
“It has been estimated that up to 90% of an application’s code is related to handling exception or error conditions. (McConnell, 2004)”
If there’s one thing that’s certain, it’s that your code will fail. Failed code is not always your fault. There’s a number of factors outside of your control that can cause failure, things like a network blip, infrastructure, outages, unhandled exceptions, cascading failures, and so on.
Writing code that can deal with these situations helps you avoid common incidents, unnecessary alerts, and noisy logs. And hopefully, give you a better night of sleep.
Handling failure is hard. In any software, there are way more error paths than successful ones. However, there are a few helpful PHP and Laravel strategies that can be implemented on any language or framework.
We can do better than programming with the expectation of success and writing TODOs to handle the errors “when we have time.”
#### **You can’t predict them all**
There’s no way to plan for every error out there. At least not upfront. You should definitely implement error handling for all the things you know can happen, and for HOW they’ll happen (a few tips for this below), but it’s impossible to predict everything that could go wrong.
#### **Logging**
Use a generalized error handling mechanism that logs errors with the most information possible. Don’t let your code fail silently.
As Alex Irvine notes, “Over-communicate. It’s better to tell someone something they already know than to not tell them something they needed to hear.”
Laravel comes with an extensive exception handler making it easy to add context, render specific errors, and so on.
#### **Monitoring & Alerts**
Just logging the errors somewhere isn’t enough. All errors should be made visible in a way that helps you notice and fix issues before your users start reporting. Centralized logging platforms like EKS, Papertrail, Datadog, etc. are key for configuring alerts to appear in different channels, such as email and slack, based on how significant the errors are.
Here are some examples of alerts triggers:
- When any CRITICAL (or above) log happens;
- When the rate of errors coming in increases;
- If NO logs with a specific message come in. This means that a key part of the system isn’t working;
- Key routines start taking more time than they should;
There’s a lot more to know about logging, monitoring, and alerting, but those are key basics. Now let’s focus on how to better deal with failures in code.
#### **Better odds with retries**
```
cURL error 6: Could not resolve host
cURL error 6: getaddrinfo() thread failed to start
```
I bet you’ve seen this error before. This type of error can happen for many different reasons outside your control. Retries are a way to prevent having your code fail in this situation, or at least give it another chance to succeed.
Let’s look at one example of dispatching a job:
```
ProcessSearch::dispatch($request->all());
```
Let’s say that AWS SQS is down for a moment and the above code fails because the HTTP request isn’t making it to the SQS. That would stop this job from getting dispatched to the queue, and you would lose the HTTP request data.
We can do better by implementing some retry logic using Laravel’s `retry` helper
```
retry(10, fn () => ProcessSearch::dispatch($request->all()), 100);
```
Here, we’ll try 10 times with a 100ms interval between each try, before the code can fail.
Another great use of the `retry` helper is when you’re using a service like Algolia for searching. Algolia can fail at any time, but usually the failures are quick and it swiftly recovers. So, applying some kind of exponential backoff using this function would make it pretty handy:
```
$locations = retry(
5,
fn () => $this->search($request->get('q')),
fn ($attempt) => $attempt * 50
);
```
Retry logic can be implemented in a lot of different places. Let’s look at one example of a retry logic using Laravel’s queued jobs with an approach using [exponential backoff.](https://en.wikipedia.org/wiki/Exponential_backoff)
```
class ProcessSearch implements ShouldQueue
{
public $tries = 5;
public function handle()
{
try {
$this->processSearch();
} catch (SearchEngineUnavailableException $e) {
$this->release($this->jobs->attempts() * 15);
}
}
}
```
#### **Safe retries with idempotent routines**
Using this method, certain operations can be executed multiple times without changing the final result. For example, this code ensures that running the same job multiple times won’t charge your customer multiple times or keep sending the same email over and over.
Let’s say you have a queued job that charges the customer and sends a confirmation email.
```
class ChargeAndSendEmail implements ShouldQueue
{
public $tries = 3;
public function __construct(User $user, Order $order)
{
$this->user = $user;
$this->order = $order;
}
public function handle(PaymentProvider $provider)
{
$provider->charge($this->user, $this->order);
Mail::to($this->user)->send(new OrderApproverMail);
}
}
```
Using this code, if the email provider is down, the job will fail; since the snippet is configured to try 3 times, it would charge the user 3 times.
Sometimes, a simple check can do the trick:
```
public function handle(PaymentProvider $provider)
{
if (! $this->order->charged()) {
$provider->charge($this->user);
}
Mail::to($this->user)->send(new OrderApproverMail);
}
```
We could also instead of actually sending the email directly from this job, queue this job, so if an error like the email provider is down, we would be able to easily retry only the email notification.
This can come in handy when updating a database record via a job payload as well.
Let’s say you have a system that receives thousands of payloads to create/update/delete documents. Sometimes, for a number of reasons, these jobs fail. But if we re-run the jobs a few hours later, a new update for the document could have been executed already, with newer data. So re-running the old job could actually override the new data.
```
class UpdateDocumentJob implements ShouldQueue
{
public function __construct($document, $payload)
{
$this->document = $document;
$this-> payload = $payload;
}
public function handle()
{
if ($this->document->updated_at > $this->payload->updated_at) {
Log::info("[UpdateDocumentJob] Not updating document because document last updated is greater than the payload last updated", [
'document' => $this->document->id,
'document_updated_at' => $this->document-> updated_at,
'payload_updated_at' => $this->payload->updated_at,
]);
return;
}
}
}
```
#### **It can still fail**
Even with retries, your code can still fail. In a situation where something like AWS `us-east-1` is down, the best you can do is have a plan in place to deal with these types of situations. How bad is it if you lose that request data? How can you save it?
Simply logging some of the data in case of final failure can make it possible to get things running quickly when all systems are stable again.
If you are using Laravel, make sure to keep an eye on your `failed_jobs` table. It can have some very valuable informations on things that are failing and you may not even know they are failing. You can also use something like the failed job monitor to get notified of those things. Also you can use our queue-batch-retry package to retry specific jobs in batches and save some of your precious time.
#### **Exceptions are your friend**
**Exceptions are not just errors**. Errors are things we try to prevent, while exceptions can happen even in the most stable environment.
One common exception I see in code is a return of either the resource, `false`, or `null`. This is fine, but what happens when you start having more than just two states (success or failure?). How do you get better information about an error with this type of approach?
```
$contact = $this->marketingSystemClient->createContact($data);
if (! $contact) {
Log::error('Error creating contact');
}
```
Is it an API error? Validation error? Network blip? Without enough data it becomes hard to deal with exceptions. The key to handling failure well is to know which failures to handle.
```
try {
$contact = $this->marketingSystemClient->updateContact($data);
} catch (GuzzleHttp\Exception\RequestException $e) {
if ($e->getStatusCode() === 404) {
$this->marketingSystemClient->createContact($contact);
}
} catch (RateLimitException $e) {
$this->release($e->getRetryAfterInSeconds());
}
```
By throwing and catching exceptions we know how to handle, it becomes very easy to read and extend this type of approach to handle other error situations as we identify them. As you gather experience with different errors and exceptions, you’ll gather more strategies for solving and preventing them.
By throwing specific exceptions, you can create custom methods like `getRetryAfterInSeconds` which make it very clear and easy to read.
##### **Domain-based exceptions**
Throwing domain-based exceptions makes it a whole lot easier to understand and plan for exceptions within your code. Check this out:
```
try {
$contact = $this->marketingSystemClient->updateContact($data);
} catch (MarketingSystem\NotFoundException $e) {
$this->marketingSystemClient->createContact($data);
}
```
Using domain-based exceptions, you can hide implementation details, while making it a lot more manageable and readable for the user or your own application.
If your error handling code feels a bit messy, ask yourself if you can maybe throw a specific exception or two at lower levels.
##### **Handle exceptions as close to the top as possible**
The closer to the top you are when actually handling exceptions (retry, send notifications, update state in the database, etc), the better. For instance, catching an exception in the controller makes it very easy to return an error response to the user. Catching an exception in the queued job makes it very easy to retry it as needed.
That doesn’t mean the rest of your code (API Clients, Actions, Services) shouldn’t be catching exceptions. Ideally, they are catching and throwing domain-based exceptions as mentioned above while hiding as much of the unneeded details as possible.
##### **Laravel renderable & reportable exceptions**
Laravel has this really awesome feature of renderable and reportable exceptions where you can define a render and report method in the exception. If exceptions bubble up to the global exception handler, it will report and/or render the response based on those methods.
```
class PaymentFailedException extends Exception
{
public function render($request)
{
return view('orders.errors.payment');
}
}
```
#### **Use Static Analysis**
Running static analysis on your CI pipeline on every commit/pull request can catch a lot of things, and help you write code that’s easier for your IDE to understand. However, starting with static analysis and implementing it on an existing application can be a complicated and annoying process. Nuno Maduro has a great talk on the subject with practical tips on how to get started.
The best tools for the PHP ecosystem are PHPStan and Psalm.
#### **Deal with them**
> “There are known knowns. These are things we know that we know. There are known unknowns. That is to say, there are things that we know we don’t know. But there are also unknown unknowns. There are things we don’t know we don’t know.” - Donald Rumsfeld
Most of the failures that you deal with won’t occur while writing new features, but when your application is running. You can identify failures through user reports, logging, monitoring, testing, etc; and when you do identify them, deal with them.
Errors can be caused by anything from a bug or infrastructure issue, to a problem with a third-party service. If the cause of the error is a bug, ship the fix with a test and make sure it doesn’t happen again. When the infrastructure is the problem, you can fix it and add monitoring around it to get notified before the problem can happen again. Issues coming from a third-party service can be solved by some good retry logic and doing your best to have the service fix itself without you or your team having to jump in to fix it.
Programming by coincidence can create an issue by allowing you to create a temporary fix without truly understanding the problem. Avoid future issues by building code that makes sense within your application, otherwise it’s likely to come back to bite you down the line.
Have a process to deal with bugs and failures as well as a process for catching and solving incidents and exceptions. Knowing how to respond, who should respond, when to escalate, the best way to communicate when things go wrong can help ensure that when errors happen, you’re ready to handle them. Share knowledge learned with incidents through post-mortems and follow-up actions to craft a system that can handle any failure.
### Extending PHP enums with attributes
*Published on December 16, 2023*
---
With the release of PHP 8.1, the language gained native support for enums. Enums are a type-safe, readable and efficient way to encapsulate a small set of possible values a field can take in your data model. Using classes instead of database enums provides more flexibility if you need to add to the list in the future.
As an example, you have a user data model and that user might have a specific list of roles you can choose from.
```
namespace App\Enums;
enum UserRole: string
{
case Admin = 'admin';
case TeamAdmin = 'team_admin';
case Support = 'support';
case Basic = 'basic';
}
```
In your data model, Laravel also has support for enums by casting them to the value if you define it in your casts array.
```
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'role' => UserRole::class,
];
```
Adding the enum casting will ensure that an exception will be thrown if we try to save a role to our user model not defined in the enum.
A very practical use of enums is to generate values for a dropdown in your HTML
```
```
On the surface, nothing seems wrong with the example above until you look at the visible name of each option in the dropdown. **Admin** and **TeamAdmin** are great variable names, but **Administrator** and **Team Administrator** would be better to present in the UI, so it is crystal clear what the role is to the person managing user roles.
While enums are great for simple name/value pairs, in cases like this where you need to add a 3rd property, you have to get creative.
Enter PHP attributes. Borrowed from the concept of annotations in other languages, it is a way to associate metadata to properties, methods and classes, which sounds exactly like what we need.
First, we need to build a Description attribute.
```
namespace App\Enums\Attributes;
use Attribute;
#[Attribute]
class Description
{
public function __construct(
public string $description,
) {
}
}
```
We now have a Description attribute we can leverage in our enum so we can define the user-friendly role names we desire.
```
namespace App\Enums;
enum UserRole: string
{
#[Description('Administrator')]
case Admin = 'admin';
#[Description('Team Administrator')]
case TeamAdmin = 'team_admin';
case Support = 'support';
case basic = 'basic';
}
```
Now we need to retrieve these attributes, which can only be done via reflection. Since we may want to reuse this attribute on other enums, we will want to make a trait to make this easier.
```
namespace App\Enums\Concerns;
use Illuminate\Support\Str;
use ReflectionClassConstant;
use App\Enums\Attributes\Description;
trait GetsAttributes
{
/**
* @param self $enum
*/
private static function getDescription(self $enum): string
{
$ref = new ReflectionClassConstant(self::class, $enum->name);
$classAttributes = $ref->getAttributes(Description::class);
if (count($classAttributes) === 0) {
return Str::headline($enum->value);
}
return $classAttributes[0]->newInstance()->description;
}
}
```
If we break this method down, the first 2 lines are using reflection to get the attributes of the enum. Since not every enum may have a Description attribute, we set up a fallback to transform the value (or name) of that enum as our description.
Lastly, we pull the value of the description from the enum attributes. We can add another method to our trait to handle this.
```
/**
* @return array
*/
public static function asSelectArray(): array
{
/** @var array $values */
$values = collect(self::cases())
->map(function ($enum) {
return [
'name' => self::getDescription($enum),
'value' => $enum->value,
];
})->toArray();
return $values;
}
```
Now, in our HTML, we can simply change the method we call on the enum class
```
```
While they are relative newcomers to PHP, enums and attributes are great additions to the language and provide native support for many common use cases.
### How to build sequences with Laravel pipelines
*Published on December 17, 2023*
---
Recently, the Laravel team added a section in the documentation about pipelines, and I think it is a good opportunity to talk about pipelines and how useful they are.
#### **But first, what are pipelines?**
Pipelines are a design pattern that enables the creation and execution of a sequence of operations. Much like an assembly line, where each step prepares a product for the next station along the line, pipelines are a group of functions linked together, so the output of the preceding function serves as the input of the following function. Laravel employs this pattern internally, such as in middleware.
#### **How do pipelines work?**
There is a `Pipeline` facade which provides a couple of useful methods to pipe a given input through a series of invokable classes or closures. Here is an example:
```
use App\Models\User;
use Illuminate\Support\Facades\Pipeline;
$user = User::create([...]);
$user = Pipeline::send($user)
->through([
SendWelcomeEmail::class,
SubscribeToNewsletter::class,
GenerateAvatar::class,
])
->then(fn (User $user) => $user);
```
- We use `send($user)` method to inject the initial data. In this case, the new created user.
- The `through([...])` method to provide the operations we want to execute with the input we are giving.
- Finally, the `then()` runs the pipeline and returns the result.
For each invokable class, two parameters are received: the input and the $next closure. Here is an example:
```
use Closure;
use App\Models\User;
class SendWelcomeEmail
{
public function handle(User $user, Closure $next)
{
$user->notify(new InvoicePaid($invoice));
$next($user);
}
}
```
When you invoke the `$next` closure, the next invokable class in the pipeline will be invoked. Additionally, there other useful methods you can use to setup the pipeline:
- The `pipe($pipes)` method enables you to add extra pipes into the pipeline. For example:
```
use App\Models\User;
use Illuminate\Support\Facades\Pipeline;
$user = User::create([...]);
$pipeline = Pipeline::send($user)
->through([
GenerateAvatar::class,
]);
if (! $user->isAdmin) {
$pipeline->pipe([
SendWelcomeEmail::class,
SubscribeToNewsletter::class,
]);
}
$user = $pipeline->then(fn (User $user) => $user);
```
- The `via($method)` method allows you to set the method to call on the pipes. By default, it is set to handle.
- The `thenReturn()` method runs the pipeline and automatically returns the result:
```
// Instead of using then()
$user = Pipeline::send($user)
->through([...])
->then(fn (User $user) => $user);
// You can use thenReturn()
$user = Pipeline::send($user)
->through([...])
->thenReturn();
```
- The `setContainer()` method enables you to set the container instance, this can be useful if you want to manually instantiate the `Pipeline` class, for instance:
```
// Using the setContainer() method
(new Pipeline)
->setContainer(app())
// But, you can also pass the container instance in the class constructor
(new Pipeline(app()))
```
Laravel pipelines are a powerful tool that allows developers to easily build and execute a sequence of operations in a clean and organized way. They are flexible and can be customized to fit any use case, making them a valuable addition to any Laravel project. By leveraging the pipeline pattern, developers can improve the scalability and maintainability of their code while also making it easier to implement complex and multi-step processes.
Laravel’s documentation on pipelines:
### Improving your password security
*Published on December 18, 2023*
---
Managing passwords across our digital lives is a tedious but necessary task. It can be a daunting one for a business with no clear cybersecurity policy in place. A robust policy limits the vectors of attack for your business and your clients and ensures you can take swift action to remedy the issue if there’s a breach. Here are a few recommendations we have for better password security.
#### **Use a password manager**
One of the best ways to manage your company’s digital life is with a password manager. Our password manager of choice is 1password, which gives us the right amount of controls and features.
If you’ve never used a password manager before, the idea is that you sign into your account using your email and a strong master password. Inside your account, you can keep track of all the logins to websites and services you need, as well as other sensitive information. While accessing your complete digital life with a single login may sound dangerous, password managers protect your data in several ways.
All data is encrypted at rest, so if your password manager of choice was ever breached, the data in your account would be useless without your credentials. In the case of 1password, they have an additional 34-character security key that you need if you sign in on any new device, which adds another layer to that encryption, making it unbreakable using today’s computing power.
Most of these services allow you to control the minimum length of the master password, enforce two-factor authentication to log in to the service, and provide detailed activity logs, giving businesses valuable insight into who has access to information.
The biggest benefit to using a password manager is the ability to generate long, complex, randomized passwords for all your logins. Having unique passwords for every login is the most important step you can take to protect yourself.
#### **Have an access policy**
Using a password manager is a great first step, but if a well-thought-out access control policy is not in place, you can still leave your company vulnerable when a breach occurs.
The best policy is to limit access to only those who need access to do their job. Only the people that need access to a website or service have it, but no one else does, no matter their position in the company. This policy has little to do with trust and is more about limiting opportunities for a breach. If you’re a company of 100 and all 100 employees have access to credentials for a service, all it takes is 1 of your employees to get hacked for there to be a problem. If only 5 of those 100 employees need access and are the only ones with it, you have reduced your vector of attack by 95% for that service.
#### **Use multi-factor authentication**
These days, most websites and services offer, and some even require multi-factor authentication. Your company policy should enforce using it when available, as it adds another layer of protection to your logins.
If offered, the preferred method is using an Authenticator application such as Google Authenticator, Authy, or even your password manager’s built-in OTP (one-time password) feature. OTP generates a unique code every 30 seconds that must be confirmed for you to log in to a service protected by it.
The most common methods are codes via SMS or email. While these methods are better than not enabling MFA, they both are vulnerable to other hacking methods. If someone were to gain access to your email, they would receive all MFA codes.
SMS is vulnerable to a common attack called SIM swapping. In this social engineering method, a bad actor calls your phone company pretending to be you and convinces the rep to port your number over to their SIM, thereby having access to your phone number.
#### **Conduct regular audits**
Password security is not a set-it-and-forget activity for a business. Designating someone, or a team, as the security and compliance officer within the company adds an extra layer to ensure your internal controls are being followed.
Setting aside time to review audit logs and revoking access to systems as employees change roles or leave are all operations that should be part of your internal controls. As part of the onboarding or offboarding process, setting aside a small amount of time to make these audits will further protect your business and clients.
---
These are some small steps your business can take to improve security and a small sample of the principles we follow here at Kirschbaum.
### How to validate command parameters in Laravel
*Published on December 19, 2023*
---
As Laravel developers, we create many complex commands for our applications. One question that always arises when creating commands is how to validate input parameters.
Laravel commands offer a lot of flexibility when it comes to argument and option inputs. However, ensuring that the user is passing the right parameters is an important step in creating a solid command.
Enter Laravel's `Validator` facade! This powerful tool allows us to validate input parameters in a simple and efficient way.
To demonstrate how to use `Validator`, let's create a command to create users with various input parameters. First, we will define the command signature:
```
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CreateUserCommand extends Command
{
protected $signature = 'create:user
{email : The email of the user}
{--name= : The name of the user}
{--password= : The password of the user}';
protected $description = 'Create a new user';
public function handle(): void
{
}
}
```
We define `email` as a required argument, and `name` and `password` as options.
Next, we add a `validateArguments()` method to validate the email, returning an array of validated input parameters if it is valid:
```
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Validator;
class CreateUserCommand extends Command
{
protected $signature = 'create:user
{email : The email of the user}
{--name= : The name of the user}
{--password= : The password of the user}';
protected $description = 'Create a new user';
public function handle(): void
{
$arguments = $this->validateArguments();
}
protected function validateArguments(): ?array
{
$validator = Validator::make($this->arguments(), [
'email' => ['required', 'email', 'unique:users,email'],
]);
if ($validator->fails()) {
$this->error('Whoops! The given attributes are invalid.');
collect($validator->errors()->all())
->each(fn ($error) => $this->line($error));
exit;
}
return $validator->validated();
}
}
```
We do a similar thing with the options, creating a `validateOptions()` method, where we’ll validate `name` and `password`:
```
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Validation\Rules\Password;
use Validator;
class CreateUserCommand extends Command
{
protected $signature = 'create:user
{email : The email of the user}
{--name= : The name of the user}
{--password= : The password of the user}';
protected $description = 'Create a new user';
public function handle(): void
{
//...
$options = $this->validateOptions();
}
//...
protected function validateOptions(): ?array
{
$validator = Validator::make($this->options(), [
'name' => ['nullable', 'string', 'min:2', 'max:20'],
'password' => [
'nullable',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols(),
],
]);
if ($validator->fails()) {
$this->error('Whoops! The given options are invalid.');
collect($validator->errors()->all())
->each(fn ($error) => $this->line($error));
exit;
}
return $validator->validated();
}
}
```
Finally, if the data is valid, we create the user.
Now, let’s move these methods to a `trait` called `ValidatesInputs` we can reuse it in other commands.
```
namespace App\Traits;
use Illuminate\Console\Concerns\HasParameters;
use Illuminate\Console\Concerns\InteractsWithIO;
use Validator;
trait ValidatesInputs
{
use HasParameters;
use InteractsWithIO;
public function validate(array $argumentRules = null, $optionRules = null): array
{
$arguments = $argumentRules
? $this->validateArguments($this->arguments(), $argumentRules)
: $this->arguments();
$options = $optionRules
? $this->validateOptions($this->options(), $optionRules)
: $this->options();
return [$arguments, $options];
}
protected function validateOptions(array $options = [], array $rules = []): ?array
{
$validator = Validator::make($options, $rules);
if ($validator->fails()) {
$this->error('Whoops! The given options are invalid.');
collect($validator->errors()->all())
->each(fn ($error) => $this->line($error));
exit;
}
return $validator->validated();
}
protected function validateArguments(array $arguments = [], array $rules = []): ?array
{
$validator = Validator::make($arguments, $rules);
if ($validator->fails()) {
$this->error('Whoops! The given attributes are invalid.');
collect($validator->errors()->all())
->each(fn ($error) => $this->line($error));
exit;
}
return $validator->validated();
}
}
```
Now, we can use this trait in the command like this:
```
namespace App\Console\Commands;
use App\Models\User;
use App\Traits\ValidateInputs;
use Illuminate\Console\Command;
use Illuminate\Validation\Rules\Password;
class CreateUserCommand extends Command
{
use ValidatesInputs;
protected $signature = 'create:user
{email : The email of the user}
{--name= : The name of the user}
{--password= : The password of the user}';
protected $description = 'Create a new user';
public function handle(): void
{
[$arguments, $options] = $this->validate(
argumentRules: [
'email' => ['required', 'email', 'unique:users,email'],
],
optionRules: [
'name' => ['nullable', 'string', 'min:2', 'max:20'],
'password' => [
'nullable',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols(),
],
]);
User::create([
'name' => $options['name'],
'email' => $arguments['email'],
'password' => $arguments['password'],
]);
}
}
```
Finally, let’s run our command with some invalid name and password data:
```
╰─❯ php artisan create:user luis@test.com --name=a --password=12
Whoops! The given options are invalid.
The name field must be at least 2 characters.
The password field must be at least 8 characters.
The password field must contain at least one uppercase and one lowercase letter.
The password field must contain at least one letter.
The password field must contain at least one symbol.
```
Laravel's `Validator` facade is a powerful tool for validating command input parameters. By implementing a `Validator` in our Laravel command, we can ensure that our commands receive the correct input parameters. This can help make our commands more reliable and avoid storing invalid data. By using a trait, we can extend this functionality to other commands that need validation.
With these tools in your toolbox, you can create Laravel commands with confidence, knowing that they will be able to handle unexpected input data while still performing the desired action.
### I’m adding a second server to my app. What now?
*Published on December 20, 2023*
---
Adding a second server to your app can be a great way to improve your app's performance and/or increase its reliability. However, there are a couple of things you need to keep in mind when adding a second server.
In this article, we'll discuss the key things you need to consider when adding an additional server to your app. We’ll use a [Laravel](https://laravel.com/) hosted in [Laravel Forge](https://forge.laravel.com/) as the example here, but the concepts can be applied to any kind of application, not even limited to the PHP language.
#### **Current infrastructure**
First, to make sure we are speaking the same language, this is the outline of the current infrastructure. This app is currently running on a server created by Laravel Forge and running on AWS.
- Lets Encrypt for the SSL certificate;
- Redis (installed on the machine) for sessions, caching and as the queue driver for storing and processing background jobs;
- MySQL (installed on the machine) as the database;
- Local folder for saving user uploaded content;
- Laravel Scheduler using server’s CRON every minute;
- Deployments are manually triggered by clicking Laravel Forge’s “Deploy now” button;
#### **1. Load balancer**
The first thing you will need is a load balancer. This will be the entrypoint of your application, meaning you will point your domain DNS to the load balancer instead of the server directly. The job of a load balancer is, as you guessed, to balance the incoming requests between all the healthy and registered servers.
From now on, every time we mention “App Server”, this will be referring to a single server running our Laravel application.
One of the nice features of a load balancer is the health checks, which serve the purpose of making sure that all connected servers are healthy. If one of the servers fails for some reason, some unscheduled maintenance for example, the load balancer will stop routing requests to that server until the server is up, running, and healthy again.
We recommend using the application load balancer, which gives more robust functionality down the road, if you need it. Application load balancers can route traffic to specific servers based on the requested URL and even route requests to multiple applications. For now, we will have it evenly balance traffic using the round robin method.
Since your domain will now be pointing to the load balancer, your SSL certificate should also be in the load balancer now, instead of in your servers.
#### **2. Database (MySQL), cache & queue (Redis)**
Currently, there is one server running our app, local instances of MySQL, and Redis. What happens when the second gets attached to our load balancer?
Having multiple sources of truth for our database and caching layers could generate all kinds of issues. With multiple databases, the user would be registered in one server but not the other. With one Redis instance per server, you could be logged in into App Server 1, but when the load balancer redirects you to App Server 2 you would have to sign in again, since your session is stored in the local Redis instance.
We could make App Server 2, or any future App Servers connected to our load balancer, connect to App Server’s 1 services, but what happens when App Server 1 has to go down for maintenance or it unexpectedly fails? One of the reasons to add a second server is to have more reliability and scalability, which does not solve our problem.
The ideal scenario, when we have multiple app servers, is to have external services like MySQL and Redis running in a separate environment. To achieve this, we can use managed services, like AWS RDS, for databases and AWS Elasticache for Redis or unmanaged services, meaning we are going to set up a separate server to run those services ourselves. Managed services are usually a better option if cost is not an issue since you don’t have to worry about OS and softwares upgrades, and they usually have a better security layer.
Let’s imagine we decided to go with managed services for our application. Our Laravel configuration would become similar as this:
```
-DB_HOST=localhost
+DB_HOST=app-database.a2rmat6p8bcx7.us-east-1.rds.amazonaws.com
-REDIS_HOST=localhost
+REDIS_HOST=app-redis.qexyfo.ng.0001.use2.cache.amazonaws.com
```
After everything is set up, our infrastructure would look like this when connecting our App Servers to our services.
#### **3. User uploaded content**
Our application allows users to upload a custom profile picture, which shows up when you are logged in. On our current infrastructure, images get saved in an internal folder in our application and also get served from there. Now that we have multiple App Servers, this would be an issue, since the images uploaded in the App Server 1 will not be present on the second server.
There are a few ways to solve this. One of them is to have a shared folder between your servers ([Amazon EFS](https://aws.amazon.com/efs/), for example). If we choose this option, we would have to configure a custom filesystem in Laravel which would point to this shared folder location on our App Servers. While a valid option, this requires some knowledge to set up the disk on the servers, and for every new server you set up, you would have to configure the shared folder again.
We usually prefer using a Cloud Object Storage service instead, like [Amazon S3](https://aws.amazon.com/s3/) or [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces). Laravel makes it really easy to work with these services, if you are using the [File Storage](https://laravel.com/docs/10.x/filesystem#main-content) options. In this case, you would only have to configure your filesystem disk to use S3, and upload all your previous user uploaded content to a bucket.
```
-FILESYSTEM_DISK=local
+FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret-access-key
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket-name
```
All your user uploaded content will be stored in the same, centralized bucket. S3 has built in versioning, multiple layers of redundancy and any additional app servers we add to our load balancer can use the same bucket to store content.
If your application grows in the future, you can set up [AWS Cloudfront](https://aws.amazon.com/cloudfront/), which acts as a CDN layer sitting on top of your S3 bucket, serving your bucket content faster to your users and often cheaper than S3.
#### **4. Queue workers**
In step 2, we set up a centralized Redis server, which is the technology we were using to manage our application queues. This will also work for our load balanced applications, but there are a few good options to explore.
If you continue to process your queues on your app servers leveraging the centralized Redis instance, no changes need to be made. The jobs will get picked up by the server that has a worker available to process a job.
Another option is to use a service like AWS SQS, which can relieve some pressure on your Redis instance as your application grows by offloading that workload to another service.
#### **5. Scheduled commands**
When running multiple servers behind a load balancer, scheduled commands would run on each server attached to your load balancer by default, which is not optimal. Not only would running the same command multiple times be a waste of processing power, but could also cause data integrity issues depending on what that command does
Laravel has a built-in way to handle this scenario so that your scheduled commands only run on a single server by chaining a **onOneServer()** method.
```
$schedule->command('report:generate')
->daily()
->onOneServer();
```
Using this method does require the use of a centralized caching server, so Step 2 is critical to making this work.
#### **6. Deployment**
When it comes to deploying your application, you now have so many options and things to consider.
We can still deploy our applications using our previous approach, but now we have to make sure we remember to click the deploy button on both servers. If we forget, we would have our servers running different versions of the application, which could cause huge issues.
With multiple servers, it’s probably time to level up the deployment strategy. There are some very good deployment tools and services out there, like [Laravel Envoyer](https://envoyer.io/) or [PHP Deployer](https://deployer.org/). These types of tools and services allow you to automate the deployment process across multiple servers, so you can remove human error from the equation.
If we want to go one level deeper in our deployment process, since we now have 2 app servers, one of the great benefits is that we can temporarily remove one of the servers from the load balancer, and that server will stop receiving requests. This allows us to have zero downtime deployment, where we remove the first server from the load balancer, deploy the new code, put it back into the load balancer, remove the second one and do the same process again. Once server 2 is finished, both servers will have the new code and will be attached to the load balancer. To achieve this, we would use tools like [AWS CodeDeploy](https://aws.amazon.com/pt/codedeploy/), but the setup is more complex than our previous options.
Deployment is a very important process of our applications, so if we can automate the deployment using Github Actions or any CI/CD services out there, we are greatly improving the process. Making the deployment process simple and where anyone can trigger a deployment really shows the maturity of the development team and the application.
#### **7. Network & security**
One additional benefit we have with the use of a load balancer is that our servers are not the entrypoint of our websites anymore. This means we can only have our servers be internally accessible and/or restricted by specific IPs (our IPs, Load Balancer IPs, etc). This greatly improves the security of our servers since they are not directly accessible. The same can (and should) be done for our database and cache clusters.
To achieve this, we are going to only allow traffic to port 22 from our own IPs (so we can SSH into the server) and we are going to only allow traffic to port 80 from the load balancer, so it can send requests to the server. The same rules apply for our database and cache clusters.
#### **Final thoughts**
There are a lot of things to consider when adding additional servers to your infrastructure. It adds more complexity to your infrastructure and workflows, but it also increases the reliability and scalability of your application as well as improves your overall security.
When considered from the beginning of the process, these recommendations are simple to implement and can have a large impact on improving your app.
### Kirschbaum partners with Filament
*Published on December 20, 2023*
---
We're excited to announce that Kirschbaum is Filament's official development agency partner!
If you've used Filament, you know how useful a tool it is for Laravel development. If you're unfamiliar, Filament is a collection of beautiful full-stack components built with the TALL stack that offers the perfect starting point for app development. From panel, table, and form builders to actions, widgets, and notifications, Filament's components deliver a well-designed framework for your Laravel app.
With Dan Harrin as a member of the Kirschbaum team, and Filament’s ability to accelerate the delivery of scalable web apps, we see this as a perfect partnership. Through our relationship with Filament, Kirschbaum will supply broader project consultation and development services to clients using or intrigued by the framework. We're continually impressed by Dan Harrin and the Filament team's ongoing work as its community grows, and we’re thrilled to sponsor Dan and his work through Kirschbaum.
A longtime Laravel partner, the Kirschbaum team gets products and services to market quickly, while anticipating the need to scale and adapt as business requirements evolve. As an early Filament adopter and promoter, Kirschbaum has leveraged the tool on large, complex projects for fast, scalable, and impactful development that delivers results.
"As the official development agency of Filament, we are thrilled to make this commitment to Dan, Filament, and the Laravel community by providing Dan dedicated time and resources to build the Filament platform. We see this as a great opportunity to continue our investment in the open source community. We’ve seen a significant increase in large and enterprise projects choosing Filament.” - Nathan Kirschbaum
"Since the beginning, I've wanted Filament to serve the Laravel community in the best way it can. This partnership with Kirschbaum really backs that promise, allowing me to sustainably dedicate time to it every day. I believe the users of this open source package will start to feel the benefits too, with faster responses to bug reports and more frequent new features." - Dan Harrin
### Strategic advantages of dynamic software
*Published on December 21, 2023*
---
The most compelling advantage of custom software is that it is built to solve your specific business problems. It minimizes the workarounds necessary for making off-the-shelf solutions deliver the results your business needs. For many businesses, this translates into improved operational efficiency, reduced overhead costs, and better insights into their business.
Custom web applications can deliver powerful market differentiation, improve your customer experience, and scale to match your needs and resources over time. Your tailored software is built to solve your unique business obstacles and can directly address any number of company-specific or industry-wide challenges.
It’s important to note that custom web applications are iterative in nature. They don’t solve every problem on day one, and that’s a good thing. Software development can’t predict the future and works best when you start by solving smaller problems and then expand as needed. Open-minded development and app scalability can position your business for long-term success.
##### How custom software can add value to your business.
No matter your industry, dynamic app development can shorten sales cycles, improve customer support, increase order deliverability, and reduce costs. Web apps designed for your specific offering and target audience can optimize your processes to deliver smoother experiences that keep customers coming back, enhance internal processes to maximize your resources and save you time and money through automation.
Whether your custom solutions are created from scratch or built on top of an off-the-shelf solution, customer-facing web apps can help with everything from speeding up fulfillment and reliably handling perishable deliveries to interactive custom ordering. Regardless of your business vertical, the unique features that tailored development can bring offer powerful customer support enhancements.
As your industry and business grow, what you need from your technology will also evolve. When you work with a development agency to build custom web app solutions, you get scalable support and technology. Creating new features and building up the capacity of your systems extends the advantages of your software, even as your business changes.
##### Conclusion
Custom web applications allow businesses to fine-tune their customer journey and provide unique experiences around their products. From customer service enhancements to operations streamlining, tailored software can bring a wide range of benefits to your business.
### How to build a CSV export system with Laravel
*Published on May 2, 2024*
---
Exporting data from a web application is a common requirement. It's often necessary to provide users with a way to download data from the application, so they can use it in other applications, or to provide a backup of their data. However, exporting data can be a slow and memory-intensive process, especially when dealing with large datasets. In this article, we will explore how to build an efficient export system in a Laravel application, which can handle large datasets without running into memory issues, and can be queued to run in the background.
The article is comprehensive. It will start by building a simple system, and make many small changes to refactor it, alongside the reasoning behind those changes. This is to help you understand the process of building an export system, and the problems you may encounter along the way. It is a window into the investigative process of building a feature, and not a quick tutorial. If you're looking for the final code or information about a pre-built export system, check out the conclusion.
#### The baseline application
The idea behind this export system example is to allow the user to export a list of "users" that belong to a "team". The table contains the columns `name`, `email` and `role`. `name` and `email` are columns on the `User` model, whereas the `role` is a pivot column that lives on the intermediate `team_user` table. This pivot table is used by the `BelongsToMany` relationship between the `Team` and `User` models.
The app uses Livewire, since it makes it straightforward to hook up an interactive interface to the export system, but this guide applies to any Laravel application. We will also be using the [Filament Notifications](https://filamentphp.com/docs/notifications/installation) package, which is built for Livewire to provide an easy way to serve flash (session) and database notifications to the user. You do not need to use either package and can instead use Laravel’s built-in notifications instead if you wish.
The UI from which the export is being triggered contains a table of users, with a search input to filter the users by name or email address. The table also contains a button to trigger the export. When the export is triggered, the user should receive a notification that the export is in progress, and then another notification when the export is complete, with a link to download the CSV file. When searching for users, the export should only contain the users that match the search query. There is a button in the interface to open a modal containing the database notifications for the user.
If you wish to follow along, this article expects you to have a good understanding of PHP, Laravel, and especially the Laravel queue system. Your app's environment should be set up with the following:
- A private S3 bucket configured for storage.
- A Redis-based queue, preferably also using Laravel Horizon for monitoring and debugging.
- The `job_batches` table in the database.
You can find the baseline UI for the application in the `starting-point` [branch of the GitHub repository.](https://github.com/kirschbaum-development/export-system-demo/tree/starting-point)
#### Synchronous exports
In PHP, the `league/csv` package is standard for reading and writing CSV files. To install it:
```php
composer require league/csv:^9.0
```
To start with, we will create a CSV file and stream it to the user immediately as a file download.
Since I am using Livewire, I will create a new `export()` method on the Livewire component, which will handle the export. However, if you're using a traditional POST request with a controller, this will work similarly.
We need to start by getting all the data we want to put in the CSV. Let's save a `$header` array containing the names of each column for the first row:
```php
$header = ['name', 'email', 'role'];
```
Now, we will get the data from the database. Like I mentioned previously, my Livewire component supports searching for users by their name or email address. We want to take the search results into account when exporting the data shown in the table, so we can reuse the getQuery() method I have on my components. Since we’re exporting users and their roles in a specific team, we want the `BelongsToMany` scoped relationship instance. (You can also get a standard Builder from `getQuery()` if you don’t need any relationship data.)
We need to run the query to get the results, and then map each of those Eloquent records into an array, containing the values of the columns we want to export. We can use the `map()` method on the Eloquent collection to do this:
```php
use App\Models\User;
$records = $this->getQuery()->get()->map(fn (User $user): array => [
$user->name,
$user->email,
$user->pivot->role,
])->all();
```
Each array in the `$records` array will represent a row in the CSV file, and each value in the array will represent a cell in the CSV file. The first value in each array will be the first column in the CSV file, the second value will be the second column, and so on, so they correspond to the values in the `$header` array.
Now we have the data we want to export, we can create a new CSV Writer instance using the createFromString() method, and then use the `insertOne()` method to insert the header row, and the `insertAll()` method to insert all the record rows:
```php
use League\Csv\Writer;
$csv = Writer::createFromString();
$csv->insertOne($header);
$csv->insertAll($records);
```
Finally, we can stream the CSV to the user as a file download using the `streamDownload()` method on the `response()` helper:
```php
return response()->streamDownload(
fn () => print($csv->toString()),
'users.csv',
['Content-Type' => 'text/csv'],
);
```
Since I am using Livewire and want to provide some immediate feedback to the user, I am also going to use a Filament flash notification to alert the user that their export is completed and is downloading, but this is optional:
```php
use Filament\Notifications\Notification;
Notification::make()
->title('Export completed')
->body('Downloading...')
->info()
->send();
```
#### Queued exports
While this system works well for small datasets, it will not perform the best for exporting large datasets. Also, it would be a good idea if we could abstract the export process so that other datasets can be exported in other parts of the application, without duplicating code.
The next iteration of the export system will be a queued job, which writes the CSV file into the filesystem (S3), and then sends a database notification to the user with a link to download the file. The S3 file will be private, so we will generate a temporary signed URL using S3 to allow the user to download the file for 24 hours, after which it will expire.
First, we will create a new job using the `make:job` Artisan command:
```php
php artisan make:job ExportJob
```
This job will be able to handle any type of export, so we need a few things:
- The model that we want to export
- The header of the CSV file
- Which database rows (records) to export
- A function to transform the database rows into CSV rows
- The current user, so we can send them a notification
We will pass all of this information to the job as constructor arguments, using property promotion:
```php
use App\Models\User;
use Closure;
public function __construct(
protected string $model,
protected array $header,
protected array $records,
protected Closure $mapper,
protected User $user,
) {}
```
Now, we can dispatch the job from the Livewire component, passing the information we need to the job:
```php
use App\Jobs\ExportJob;
use App\Models\User;
dispatch(new ExportJob(
model: User::class,
header: ['name', 'email', 'role'],
records: $this->getQuery()->pluck('users.id')->all(),
mapper: fn (User $user): array => [
$user->name,
$user->email,
$user->pivot?->role,
],
user: auth()->user(),
));
```
The main new part of this code is the `records` array, which contains the IDs of the records we want to export. We are using the `pluck()` method to get an array of user IDs from the query, which we will pass to the job. The job will then use these IDs to retrieve the records from the database. This is more efficient than retrieving all the model instances in a collection and passing them to the job, as it reduces the amount of data in memory.
However, when we dispatch the job, we get an error, since `Closure` objects are not serializable. Closure objects represent functions in PHP, in this case the `mapper` function. Serialization is the process of converting data (usually objects) into strings, so they can be sent to Redis for temporary storage before the queued job gets processed. We can't serialize a Closure object, so we need to find a way to pass the function to the job in a different way.
Luckily, Laravel has a solution: their [Serializable Closure](https://github.com/laravel/serializable-closure) package is able to wrap a function and make it serializable. We can install it using Composer:
```php
composer require laravel/serializable-closure
```
It's simple to be able to serialize the `mapper` function using the package, by wrapping it in a `SerializableClosure` instance in the constructor, and storing that in a property instead of the original function:
```php
use Closure;
use Laravel\SerializableClosure\SerializableClosure;
protected SerializableClosure $mapper;
public function __construct(
protected string $model,
protected array $header,
protected array $records,
Closure $mapper,
protected User $user,
) {
$this->mapper = new SerializableClosure($mapper);
}
```
Now, in the `handle()` method of the job, we can move the CSV generation code from the Livewire component to the job:
```php
use League\Csv\Writer;
$csv = Writer::createFromString();
$csv->insertOne($this->header);
$csv->insertAll(
$this->model::find($this->records)
->map($this->mapper->getClosure())
->all(),
);
```
We pass the CSV data to the insertAll() function. Let's explore that process in more detail:
- `$this->model` contains the fully qualified class name of the model we want to export
- Since `find()` is a static method available on model classes, we can call that directly on the model class name. If it was an instance method, we'd have to instantiate the model first. Usually, developers only pass one ID to the `find()` method of a model or query, to fetch a singular model instance. But did you know, you can instead pass an array of IDs, which will return a collection of all the corresponding model instances?
- From the collection of model instances, we can then map them to the CSV rows using the `mapper` function. We use the `getClosure()` method on the `mapper` property to get the original function back from the `SerializableClosure` instance, and then call it using the `map()` method on the collection.
- Finally, we call the `all()` method on the collection to get an array of the CSV rows, which we pass to the `insertAll()` method of the CSV writer.
Now, let's store the CSV file privately in an S3 bucket. We need to generate a unique file name for this export, so we should use a random string as part of the file name. We can also add the name of the model we are exporting to the file name for aesthetics:
```php
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
$fileName = (string) str($this->model)
->classBasename()
->plural()
->snake()
->append('-')
->append(Str::random())
->append('.csv');
Storage::put("exports/{$fileName}", $csv->toString(), options: Filesystem::VISIBILITY_PRIVATE);
```
Finally, let's generate a temporary signed URL for the file, and notify the user. I am using Filament database notifications to send the notification, but you can use Laravel's built-in notifications if you prefer:
```php
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
$csvUrl = Storage::temporaryUrl("exports/{$fileName}", now()->addDay());
Notification::make()
->title('Export completed')
->actions([
Action::make('download')->markAsRead()->url($csvUrl),
])
->success()
->sendToDatabase($this->user);
```
#### Serializing the query
There is a bug in our queued implementation. You may have noticed that when we started queueing the export, we switched from fetching the user's role with `$user->pivot->role` to fetching it with `$user->pivot?->role`. This is because the `pivot` property is not available on the model instances when they are retrieved from the database in the queued job. The` pivot` property is added to the model instances by Laravel when they are retrieved from a relationship, and this does not happen when the model instances are serialized and unserialized by the queue.
The flaw that needs fixing is with the array of record IDs we are passing to the job. The original query used to fetch the records from the database has been lost, and that was the easiest way that we can fetch the pivot data for the records. We need to find a way to pass the original query to the job, so that it can be executed again inside the job.
Laravel uses query builder objects to represent database queries, and these objects are not really serializable out of the box. However, there is a package you can use called [Eloquent Serialize](https://github.com/AnourValar/eloquent-serialize), which can serialize and unserialize Eloquent query builder objects, allowing us to pass them to queued jobs.
We can install the Eloquent Serialize package:
```php
composer require anourvalar/eloquent-serialize
```
Let's pass the query into the job instead of the model, since the query builder object contains the name of the model it is querying anyway:
```php
use App\Jobs\ExportJob;
use App\Models\User;
dispatch(new ExportJob(
query: $this->getQuery(),
header: ['name', 'email', 'role'],
records: $this->getQuery()->pluck('users.id')->all(),
mapper: fn (User $user): array => [
$user->name,
$user->email,
$user->pivot?->role,
],
user: auth()->user(),
));
```
And in the constructor of the job, serialize the query before storing the serialized query string in a property:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use App\Models\User;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Laravel\SerializableClosure\SerializableClosure;
protected string $query;
protected SerializableClosure:: $mapper;
public function __construct(
Builder $query,
protected array $header,
protected array $records,
Closure $mapper,
protected User $user,
) {
$this->query = EloquentSerializeFacade::serialize($query);
$this->mapper = new SerializableClosure($mapper);
}
```
However, now when dispatching the job, we get an error, because in my case the `getQuery()` method returns a `BelongsToMany` relationship, and the `BelongsToMany` relationship is not a query builder that is serializable by the Eloquent Serialize package. We need to convert the relationship to a query builder before serializing it:
```php
query: $this->getQuery()->getQuery(),
```
There is a difference in the way pivot data is returned when querying a query builder instead of a relationship. Since the query builder has no knowledge of the relationship, it does not serialize a pivot model for each model instance. The pivot data is joined to each model instance, so is accessible as any other property on the model, instead of being nested inside a `pivot` relation. We need to update the `mapper` function to reflect this:
```php
mapper: fn (User $user): array => [
$user->name,
$user->email,
$user->role,
],
```
Finally, we need to adjust the `handle()` method of the job to unserialize the query and use it to fetch the records from the database instead of the model class name. We also need to refactor the file name generation to fetch the model from the query builder. It now looks like this:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use League\Csv\Writer;
$query = EloquentSerializeFacade::unserialize($this->query);
$csv = Writer::createFromString();
$csv->insertOne($this->header);
$csv->insertAll($query->find($this->records)->map($this->mapper->getClosure())->all());
$fileName = (string) str($query->getModel()::class)
->classBasename()
->plural()
->snake()
->append('-')
->append(Str::random())
->append('.csv');
Storage::put("exports/{$fileName}", $csv->toString(), options: Filesystem::VISIBILITY_PRIVATE);
$csvUrl = Storage::temporaryUrl("exports/{$fileName}", now()->addDay());
Notification::make()
->title('Export completed')
->actions([
Action::make('download')->markAsRead()->url($csvUrl),
])
->success()
->sendToDatabase($this->user);
```
#### Reducing the queueing delay
Currently, we are executing a query to fetch all the IDs of the records we want to export, and then passing those IDs to the job. The user is having to wait for this query to finish before the job is dispatched, and they receive the first notification. This was originally introduced before we started passing the query to the job, because if the query was filtered by the user (they searched for users by name or email address), we would export users outside the dataset. However, now that we are passing the query to the job, we can dispatch the job immediately, and then execute the query to fetch the user model instances inside the job:
```php
use App\Jobs\ExportJob;
use App\Models\User;
dispatch(new ExportJob(
query: $this->getQuery()->getQuery(),
header: ['name', 'email', 'role'],
mapper: fn (User $user): array => [
$user->name,
$user->email,
$user->role,
],
user: auth()->user(),
));
```
We can remove the `records` argument from the constructor of the job:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use App\Models\User;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Laravel\SerializableClosure\SerializableClosure;
public function __construct(
Builder $query,
protected array $header,
Closure $mapper,
protected User $user,
) {
$this->query = EloquentSerializeFacade::serialize($query);
$this->mapper = new SerializableClosure($mapper);
}
```
And finally, replace the `find()` method with `get()` in the `handle()` method of the job, to fetch all records from the query:
```php
$csv->insertAll($query->get()->map($this->mapper->getClosure())->all());
```
#### Chunking the query
Currently, the `get()` is fetching all the records from the database in one go, and hydrating each row into a model instance. With large datasets, this query could be slow due to the amount of data being fetched and memory inefficient due to the number of model instances that need to be stored in the collection at once. We can improve this slightly by using the `chunkById()` method to fetch the records in smaller chunks, and then map the model instances into arrays from there. This will reduce the amount of memory used by model instances at once:
```php
use Illuminate\Database\Eloquent\Collection;
$recordRows = [];
$mapper = $this->mapper->getClosure();
$query->chunkById(
100,
function (Collection $records) use ($mapper, &$recordRows) {
$recordRows = [
...$recordRows,
...$records->map($mapper)->all(),
];
},
column: $query->getModel()->getQualifiedKeyName(),
alias: $query->getModel()->getKeyName()
);
$csv->insertAll($recordRows);
```
Queued jobs have a maximum execution time, and if a job takes longer than this time to execute, it will crash. Also, if there is a problem with part of the dataset and the job fails, the entire dataset needs to be exported again. Using batched jobs can solve both of these problems. We can dispatch a job to chunk the query and dispatch multiple jobs to export the chunks, and then dispatch a job to send the notification when all the chunks have been exported. We can asynchronously add chunks to the batch so that they can be processed immediately, even if the entire batch has not been prepared yet, which will provide a further performance improvement.
The way to set this up is to create a job chain containing the batch followed by the job to send the notification. The batch will be created with just one job, which will chunk the query and add the chunks to the batch.
Let's create a new job to prepare the export batch, a job to export each chunk, and a job to send the notification when the export is complete:
```php
php artisan make:job PrepareExportBatchJob
php artisan make:job ExportChunkJob
php artisan make:job SendExportNotificationJob
```
In the original `ExportJob`, we should set up the CSV file with the correct headers, since this should only happen once per export, and the rest of the jobs require the file to exist:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use League\Csv\Writer;
$query = EloquentSerializeFacade::unserialize($this->query);
$csv = Writer::createFromString();
$csv->insertOne($this->header);
$fileName = (string) str($query->getModel()::class)
->classBasename()
->plural()
->snake()
->append('-')
->append(Str::random())
->append('.csv');
Storage::put("exports/{$fileName}", $csv->toString(), options: Filesystem::VISIBILITY_PRIVATE);
```
Now, we should dispatch the job chain, containing the export batch as well as the notification job:
```php
use App\Jobs\PrepareExportBatchJob;
use App\Jobs\SendExportNotificationJob;
use Illuminate\Support\Facades\Bus;
Bus::chain([
Bus::batch([
new PrepareExportBatchJob(
$this->query,
$this->mapper,
$fileName,
),
]),
new SendExportNotificationJob(
$fileName,
$this->user,
),
])->dispatch();
```
The `PrepareExportBatchJob` job will accept the serialized query, serialized mapper, and file name for the export:
```php
use Laravel\SerializableClosure\SerializableClosure;
public function __construct(
protected string $query,
protected SerializableClosure $mapper,
protected string $fileName,
) {}
```
In the `handle()` method of the job, we will unserialize the query and mapper, and then chunk the query and dispatch ExportChunkJob instances to export the chunks. The query is converted to a non-Eloquent query builder, since we do not need to hydrate any model instances if we just need to fetch the IDs of the records in the chunk. This makes the process even faster and less memory intensive. We also need to use the `Arr::pluck()` method to convert the array of database row objects into an array of IDs:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use App\Jobs\ExportChunkJob;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
$query = EloquentSerializeFacade::unserialize($this->query);
$keyName = $query->getModel()->getKeyName();
$qualifiedKeyName = $query->getModel()->qualifyColumn($keyName);
$baseQuery = $query->toBase();
$baseQuery->distinct($qualifiedKeyName);
$baseQuery
->select([$qualifiedKeyName])
->chunkById(
100,
function (Collection $records) use ($keyName) {
$this->batch()->add(new ExportChunkJob(
query: $this->query,
records: Arr::pluck($records->all(), $keyName),
mapper: $this->mapper,
fileName: $this->fileName,
));
},
column: $qualifiedKeyName,
alias: $keyName,
);
```
> The reason why we are fetching the IDs of the records in each chunk and passing them to the job, instead of passing the job a "page number" which it can then use to paginate the query using offset and limit, is because it can result in issues with duplicate or missing records. This is because the database can be updated between the time the query is chunked and the time the chunk is processed, and the records that were in the first chunk may not be in the second chunk, or may be in both chunks. The chunkById() method prevents this by fetching each page based on the primary keys of the records at the edges of each chunk.
The `ExportChunkJob` accepts the serialized query, the record IDs to export, the serialized mapper, and the file name for the export:
```php
use Laravel\SerializableClosure\SerializableClosure;
public function __construct(
protected string $query,
protected array $records,
protected SerializableClosure $mapper,
protected string $fileName,
) {}
```
In the `handle()` method of the job, we will unserialize the query and mapper, and then fetch the records from the database and export them to the existing CSV file. Since there is no special encoding when writing to a CSV file unlike XLSX files, we can append the new rows to the existing file without corrupting it:
```php
use AnourValar\EloquentSerialize\Facades\EloquentSerializeFacade;
use Illuminate\Support\Facades\Storage;
use League\Csv\Writer;
$query = EloquentSerializeFacade::unserialize($this->query);
$mapper = $this->mapper->getClosure();
$csv = Writer::createFromString();
$csv->insertAll($query->find($this->records)->map($mapper)->all());
Storage::append("exports/{$this->fileName}", $csv->toString());
```
Finally, the `SendExportNotificationJob` job will accept the file name for the export and the user to send the notification to:
```php
use App\Models\User;
public function __construct(
protected string $fileName,
protected User $user,
) {}
```
In the `handle()` method of the job, we will generate a temporary signed URL for the file, and then send a notification to the user with a link to download the file:
```php
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Storage;
$csvUrl = Storage::temporaryUrl("exports/{$this->fileName}", now()->addDay());
Notification::make()
->title('Export completed')
->actions([
Action::make('download')->markAsRead()->url($csvUrl),
])
->success()
->sendToDatabase($this->user);
```
#### Chunking to multiple files
The current system, even though it has evolved so much, still has flaws:
- The order of jobs being executed in the batch is not guaranteed, so the chunks of the CSV file may not end up being in the correct order.
- S3 does not offer a way to append rows to an existing file, so Laravel will internally download the entire file, append the new rows, and then upload the entire file again. This is inefficient, can be slow with large files, and can result in memory limit issues.
The way to solve these problems is to avoid creating just one singular CSV file, and instead creating multiple CSV files, one for each chunk of the query. We can then use a controller to stream the chunks in the correct order into one singular file, without ever loading them all into memory at once.
The file structure in S3 will look something like this:
```php
exports/
users-h3b23pwa/
00000001.csv
00000002.csv
00000003.csv
00000004.csv
headers.csv
...
...
```
The numbered file names are padded with zeros so that they are in the correct order when sorted alphabetically. The `headers.csv` file contains the header row of the CSV file, and the numbered files contain the rows of the CSV file.
Let's start by creating the header file, in `ExportJob`. We should remove the `.csv` from the `$fileName` variables and properties everywhere, so we can append the chunk file name onto the end easier.
```php
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
$fileName = (string) str($query->getModel()::class)
->classBasename()
->plural()
->snake()
->append('-')
->append(Str::random());
Storage::put("exports/{$fileName}/headers.csv", $csv->toString(), options: Filesystem::VISIBILITY_PRIVATE);
```
In the `PrepareExportBatchJob`, we should keep track of the `$page` number of the current chunk, and pass that as a parameter to the constructor of `ExportChunkJob`:
```php
use App\Jobs\ExportChunkJob;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
$page = 1;
$baseQuery
->select([$qualifiedKeyName])
->chunkById(
100,
function (Collection $records) use ($keyName, &$page) {
$this->batch()->add(new ExportChunkJob(
query: $this->query,
records: Arr::pluck($records->all(), $keyName),
page: $page,
mapper: $this->mapper,
fileName: $this->fileName,
));
$page++;
},
column: $qualifiedKeyName,
alias: $keyName,
);
```
The `$page` variable is passed into the function by reference (`&$page`) so that it can be incremented each time a chunk is processed, otherwise the value will not be persisted between calls to the function.
The new constructor for `ExportChunkJob` will accept the page number:
```php
use Laravel\SerializableClosure\SerializableClosure;
public function __construct(
protected string $query,
protected array $records,
protected int $page,
protected SerializableClosure $mapper,
protected string $fileName,
) {}
```
We now need to handle the fact that the file has been split into multiple chunks using a controller. Let's create an invokable controller called `DownloadExportController`:
```php
php artisan make:controller DownloadExportController --invokable
```
In the `DownloadExportController`, we should double-check that the directory containing the exported files still exists, in case you wish to implement some automatic cleanup of old exports. Then, we can fetch each file in the correct order, echo it to the response, and then flush the system output buffer:
```php
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
public function __invoke(string $fileName): StreamedResponse
{
if (! Storage::exists("exports/{$fileName}")) {
abort(419);
}
return response()->streamDownload(function () use ($fileName) {
echo Storage::get("exports/{$fileName}/headers.csv");
flush();
foreach (Storage::files("exports/{$fileName}") as $file) {
if (str($file)->endsWith('headers.csv')) {
continue;
}
if (! str($file)->endsWith('.csv')) {
continue;
}
echo Storage::get($file);
flush();
}
}, "{$fileName}.csv", [
'Content-Type' => 'text/csv',
]);
}
```
Let's register a route to the controller, and ensure that the route is protected by a signature, so that the user must have a valid signed URL to access the exported file:
```php
use App\Http\Controllers\DownloadExportController;
use Illuminate\Support\Facades\Route;
Route::get('/exports/{fileName}', DownloadExportController::class)
->middleware(['signed'])
->name('exports.show');
```
Finally, update the URL generation in the `SendExportNotificationJob` to use the new route:
```php
use Illuminate\Support\Facades\URL;
$csvUrl = URL::signedRoute('exports.show', ['fileName' => $this->fileName]);
```
#### Bonus: Exporting to XLSX
The system we have built so far is designed to export to CSV files, but we can also export to XLSX files. To do so, you can chain an extra job into the chain, which runs after the batch, which collates the chunks into one singular XLSX file. There is no way to stream multiple XLSX files into one file in the same way as we can with CSV files, so you need to be aware of the memory limits of your server when using this method, or implement a way for the user to download multiple XLSX chunks in separate files. I would advise using the [OpenSpout ](https://github.com/openspout/openspout/blob/4.x/docs/documentation.md)package for generating XLSX. The alternatives like `PhpSpreadsheet`, while more feature complete, are reported to not be as fast.
#### Conclusion
If you'd like to see the entire code for the final system, you can find it [here](https://github.com/kirschbaum-development/export-system-demo). The commits in the repository mostly follow the order of the steps in this article, so you can see the changes that were made to the code to implement each feature.
It's straightforward to build a simple CSV export system. But like many things, the more data you need to handle, the more complex the system needs to become to handle this efficiently. I hope that I have demonstrated a good approach to solving that problem.
If you're looking for a pre-built export system that follows these concepts, and ships with a nice fluent API for defining exports, I maintain a project called [Filament](https://filamentphp.com/) which has a built-in export system that follows these concepts. It's a Laravel package that many developers use to build administration panels for their applications, but the components can be used in your own Blade-based applications too. You can find the documentation for the [Export action](https://filamentphp.com/docs/actions/prebuilt-actions/export), as well as [how to implement this in Livewire.](https://filamentphp.com/docs/3.x/actions/adding-an-action-to-a-livewire-component)
### How we approach DevOps at Kirschbaum
*Published on May 22, 2024*
---
In the ever-evolving environment of web development, a robust DevOps strategy is key for ensuring consistent and reliable project delivery. At Kirschbaum, our approach to DevOps for Laravel development is designed to meet the unique needs of each client, focusing on established processes, collaboration, automation, and innovation.
One size does not fit all when it comes to DevOps in consulting. We recognize that every client's needs are distinct, and our approach adapts accordingly. Whether our clients have an established DevOps team or not, we adapt our strategies to align with their existing structures and processes.
#### DevOps Assessment
Before executing any changes, we perform an extensive DevOps assessment in our client’s infrastructure and applications to make sure we fully understand their environment and are able to suggest improvements without breaking what’s already working.
#### Cloud-Agnostic
While we have a preference for AWS, our team is versatile and adept at working with any cloud provider. This flexibility allows us to accommodate our clients' preferences and optimize their Laravel (and other frameworks) applications for the cloud environment that best suits their requirements.
Our team is skilled in building custom setups using tools like Docker, Terraform, and Ansible, tailoring solutions to the specific needs of each project.
#### Production Environment
We are equipped to manage and deploy your Laravel application across any environment. Whether you're looking to utilize Laravel Forge for server management, Laravel Vapor for serverless deployment, Docker for containerization, or other common technologies and services, our developers have the skills and experience to set up, integrate and maintain your application, independently of the nature of your application.
Our commitment is to provide a flexible and reliable service that aligns with your specific requirements, ensuring your application runs smoothly, anywhere.
#### Performance and Cost Optimization
Optimizing performance and managing costs effectively stands at the heart of our DevOps services. Our methodology goes beyond deployment; it includes a strategic analysis to ensure that every aspect of your Laravel application not only performs at its peak but does so cost-efficiently.
This encompasses everything from ensuring appropriate instance sizes are established and auto-scaling is configured to prevent over-provisioning while also enabling the handling of traffic spikes without impacting performance, to ensuring Laravel routes, configurations, and events are cached, PHP is optimized, Laravel Octane was evaluated as an option and all the minor details that collectively make a significant difference.
#### Laravel Ecosystem
Kirschbaum is passionate about Laravel and its ecosystem, and we also leverage Laravel’s DevOps tools and services like Laravel Vapor, Laravel Forge, Laravel Envoyer and Laravel Pulse. We have configured countless applications of all sizes and complexities using these tools.
#### Continuous Integration (CI) and Continuous Deployment (CD)
Automation is at the core of our DevOps practices. We utilize GitHub Actions and other CI tools to automate repetitive tasks, run key checks like testing, linting, static analysis on every commit or pull request, automate deployments and avoiding single-developer bottlenecks, reliability, and faster delivery.
#### Teamwork
We always work in pairs when performing DevOps tasks in production. This way, we can help each other out, catch mistakes before they happen, and in the rare cases something goes wrong, we've got extra hands ready to fix it.
#### Continuous Monitoring and Alerting
To sleep well at night, having your application and infrastructure monitored 24/7 is indispensable. We use diverse tools and services to make sure we monitor all the important metrics for your project. Our go-to tools include Laravel Pulse, the ELK stack, CloudWatch, Prometheus, and Grafana, while services like NewRelic and DataDog rank high on our list of favorites.
#### DevSecOps and Security
Incorporating security into our DevOps practices (DevSecOps) is not an occasional activity but a fundamental aspect of our workflow at Kirschbaum. We ensure that security is embedded in our process through automation, collaboration and defined processes. We implement processes such as automatic dependency and software updates, code and secret scanning, and all of the industry best practices.
DevOps is a crucial part of any application that can't be overlooked. Your application being down can be worse than it having a critical bug. Our approach at Kirschbaum combines adaptability, automation, and a commitment to industry best practices to ensure your application is always running smoothly.
### Why we love Vue.js
*Published on June 4, 2024*
---
#### Why use Vue.js
Modern frontend web development is famously, if not notoriously, dynamic and complex. There is a myriad of libraries, frameworks, architectures, principles, and approaches from which to choose. While these options make for an exciting landscape for developers, the competing alternatives can be overwhelming for a business seeking efficient and practical solutions.
We always aim to pick the right tool for our clients' needs, and we often find that Vue.js stands head and shoulders above other frontend technologies. Here are some of the features we love -- and we think you'll love, too -- about this expressive, reactive Javascript framework.
#### Vue.js is flexible.
Vue.js is often described as the "progressive" framework. In practice, this means that it can be introduced into a project or codebase in various manners -- that is, as a standalone script, as a traditional or Inertia.js single-page application (SPA), as a server-side rendered (SSR) application, via the Vue-based Nuxt "meta-framework", and more! This versatility facilitates graceful, and even gradual, adoption, allowing for a cost-effective approach to migrating to modern frontend workflows. More importantly, Vue's adaptability empowers us to deliver made-to-order implementation strategies that can support each project's unique requirements around complexity, interactivity, and state management.
#### Vue.js is stable and dependable.
Vue has been consistently supported and fine-tuned since its original release in 2014. It has millions of monthly downloads, millions of global users, and has been implemented in numerous production projects, including by such companies as Google, Microsoft, and Apple. While this widespread adoption is impressive in and of itself, it actually has tangible value in the field when it comes to making sound choices around a technical stack. Indeed, Vue's decade of real-world usage and steady, thoughtful development bolsters our confidence -- and the confidence of our clients -- in the framework's performance, security, and reliability, all of which help to ensure we are creating web-based solutions that are both maintainable and sustainable for the long haul.
#### Vue.js is constantly improving.
Although Vue.js has reached maturity as a technology, this is not to say that it has become stale or stagnant. To the contrary, Vue.js is constantly evolving to support the ever-changing topography of both the browser and the web. This includes the introduction of new features and capabilities (such as Teleport and Suspense), improvements to performance and the rendering process, a growing ecosystem of official libraries (e.g. Pinia and Vue Router), and a healthy diversity of developer-focused tooling. These improvements, beyond ensuring that the framework stays relevant, adaptable, and future-proof, also empower us to create cutting-edge, lightning-fast, feature-rich web applications for both our clients and their customers.
#### Vue.js has a strong community.
Vue.js is a collaborative endeavor, shaped by a large and engaged team of independent contributors. While this certainly isn't a requirement for a given technology to be the best fit for a codebase, Vue's robust community delivers several meaningful advantages. At a minimum, it facilitates troubleshooting via official and unofficial support channels, encourages the creation of and easy access to learning materials and documentation, and fosters the development of an enormous range of third-party packages and libraries.
These qualities translate to real benefits for developers in that they enable us to lean on community insight around and solutions to common technical challenges. In practical terms, this means that we spend less time on run-of-the-mill technical minutia and are able to focus instead on what often matters most to our clients: building delightful, effective, and cost-efficient technical solutions for their individual business problems.
#### Conclusion
This brief list only scratches the surface of why and what we love about Vue.js!
What's more, while Vue is certainly able to stand on its own as a formidable frontend technology, it can also integrate seamlessly and flexibly with Laravel – another of our favorite tools – engendering a comprehensive solution to building dynamic, interactive, and modern full-stack web applications.
If you're eager to explore what Vue.js has to offer and how it can meet the needs of you and your business, we'd love to speak with you.
### Why we love Filament
#### A powerful tool for Laravel developers
*Published on July 26, 2024*
---
#### Introduction
It may seem trite, but it’s worth reiterating all of the good things that can be said about the Laravel community as a whole. It’s been welcoming to so many people, myself included, and provides developers with a bounty of great resources, great people, and great tools that positively impact the development experience on a daily basis.
One of those tools that you’ve likely encountered, or at least heard about, is Filament. If it’s new to you, welcome to the party! At this point, you can’t venture into too many corners of the Laravel community *without* finding it. And for good reason.
Let’s take a high-level look into Filament, what it is, its benefits, why we use it here at Kirschbaum, and how it can be helpful to you.
#### The Importance of Tools That Aid in Development
If you’re reading this, you’re likely a developer in the Laravel world, or someone interested in exploring how Filament can potentially aid your business or enhance your application. If you’re already using Laravel as your framework of choice, you’re doing so for a reason; creating good applications is difficult and good tools help make it easier.
When developers use good tools (and the *right* tools), the resulting applications become even better. Filament is an extremely versatile tool, much like a Swiss army knife, making it ideal to use in a wide range of situations.
#### What Is Filament?
Filament is a full-stack UI framework for Laravel that accelerates development by providing features and functionality that usually requires multiple packages. From rendering tables to creating forms, Filament is packed with essential features that almost every application needs.
Filament also offers a few additional benefits, such as:
- Thorough documentation that quickly directs you to a solution.
- Friendly and expected conventions that we’ve come to love in the Laravel community.
- A robust and supportive community of developers and plugin authors for when your solution might be beyond the framework’s bounds.
#### Filament’s Key Features
Filament provides a robust baseline of key features that we see in almost every application we interact with. You’ll find a great deal of quality content related to Filament’s features in its [documentation](https://filamentphp.com/docs), but here are a few of the features you’d likely be reaching for frequently:
##### Form Builder
Forms can be quite complex, and creating and maintaining them across a large codebase can be challenging. Filament offers a robust and flexible form builder that simplifies this process, enabling developers to effortlessly create and maintain high-quality forms.
Filament supports a wide variety of input fields, validation options, helpers, and lifecycle action hooks, ensuring the forms you build fulfill all of your specific needs. Perhaps best of all, Filament allows you to do all of this in a single place, without having to jump between backend controllers, form requests, and a frontend view, providing a full stack feel to the form building experience.
##### Table Builder
Tables are another common and potentially complex feature in most applications. The tables Filament provides not only look great, but are also incredibly functional and flexible, offering a wide range of features right out of the box.
With built-in support for filtering, searching, inline actions, and responsive layouts, Filament tables are designed to enhance both the end user experience, as well as developer efficiency (and happiness too).
Filament tables have a wide variety of practical applications, such as listing your application’s users, books in a library that aren’t currently checked out, or complex monthly financial reports.
##### Actions
“Action” is a fairly loaded word in the development world. In Filament, actions are components that bundle a function with the means to trigger that function in the user interface. Actions can also include any supplementary steps that a function requires, such as opening a modal window for confirmation or collecting data through a form.
Filament ships with prebuilt actions that give developers the ability to create, edit, view, delete, and replicate Eloquent models, among other features. It also includes import and export actions to handle every aspect of these generally tedious tasks, from the user interface to the queued jobs that can process large amounts of data effectively. Prebuilt actions are customizable and extensible, but if that still isn’t enough to meet the requirements of what you’re trying to achieve, Filament also provides the ability to easily create custom actions.
Custom actions can extend your application’s functionality beyond what prebuilt actions offer. For example, you can enable an admin user to preview a blog post before publication or trigger an email notification to inform a user that their document is ready for review. The possibilities are endless. This level of customization empowers you to tailor actions specifically to your application’s business domain, ensuring that your unique requirements are met with precision.
##### Custom Components
Beyond providing the amazing core features already mentioned, Filament doesn’t limit the potential of your applications by these features. In addition to a robust and ever growing plugin library, implementing custom pages is extremely straightforward and can be done with a simple command:
```php
php artisan make:filament-page CustomPage
```
Filament is built on top of Livewire and AlpineJS, so the limitations of what you can build with the framework are the bounds of what you can develop with the underlying technologies themselves. Filament provides you the means to create custom and extensible Livewire components, and do exactly what you need to do in order to make your application great.
This means it’s not only easy to get your project going with Filament, it’s easy to keep going, even when the project requirements stretch beyond what you previously thought was possible within the framework.
Perhaps best of all, Filament core components and functionality can still be integrated into the custom Livewire and AlpineJS pages you build. This makes it extremely easy to maintain consistency between out-of-the-box and custom functionality. Filament also provides a library of Blade components that will ensure custom built pages feel native to the rest of the application.
You can go out on your own to build whatever functionality your application needs, but you won’t be alone.
#### When and Why We Choose Filament
Every project requires potentially different tools and technology to succeed. At Kirschbaum, we don’t just throw a stack of technology at a problem because it’s been the solution before. Instead, we carefully consider the project requirements and the outcomes we’re aiming for to determine the most suitable tools.
In many cases, Filament provides part of the answer to the question: *What tools do we need to build this application well?*
While Filament’s core functionality seems tailor-made for implementations like admin dashboards, its flexibility and versatility allow it to be used for so much more. Adding Filament to our toolbelt has been a game changer for us and can be for you as well.
##### More Than an Admin Dashboard
Although Filament does excel at creating admin dashboard-like interfaces, it can also serve as the core of applications themselves. Filament supports the concept of multiple panels within a single Laravel application, allowing for separation between various parts of the application that may require different functionality.
This makes Filament well suited for applications requiring multi-tenancy, different user groups, or distinct functional layers. Each panel can be adapted to meet the specific needs of its users, groups, or teams, showcasing Filament’s ability to tackle a wide range of project requirements with confidence.
##### Rapid Development, Faster Iterations
Building applications with Filament is *fast*. Its robust feature set, intuitive conventions, and well-designed components enable developers to quickly build applications without compromising on quality.
This results in a faster development feedback loop, and more efficient use of development resources. Developers and clients alike appreciate focusing more time and energy on solving real business challenges rather than reinventing the foundational components of applications.
##### A Strong Core Team and a Supportive Community
The Filament core team, along with countless community contributors, work tirelessly to ensure that Filament is a great tool today, and will continue to be in the future. This support is a crucial component when deciding to incorporate a piece of technology into your stack, reassuring you that you won’t be left stranded.
You can have confidence and trust that Filament will continue to grow its feature set and stay up to date as technology evolves. Additionally, the existing support network ensures that developers can easily find solutions to any challenges they encounter, reducing developmental setbacks and keeping projects on track.
#### Summary
Filament is a robust, powerful, and versatile framework that can be used to solve a wide range of business challenges. At Kirschbaum, we were thrilled to add Filament to our toolkit, and we love using it when the situation calls for it. Not every situation does, but when a project shines the Filament bat signal and it’s thrust into action, it’s an indispensable tool that helps us deliver high-quality, enterprise-level applications.
I hope this article provided you a bit of insight into what Filament is, why a team like Kirschbaum and developers like myself are turning to it to solve problems, and how it might be able to help you solve some challenges you might be facing with your own projects.
So now that you know why we use Filament, if you don’t already, why not give it a try?
### Tailwind CSS vs Bootstrap: Which is Better for Your Project?
*Published on September 11, 2023*
---
#### Tailwind CSS vs Bootstrap Which CSS Framework Should You Choose?
In the fast-paced world of web development, CSS frameworks have become crucial tools for developers. They streamline the design process, making it easier to create responsive and aesthetically pleasing websites. However, with several options available, choosing the right framework can be challenging. For many developers, this choice often comes down to Tailwind CSS vs Bootstrap.
In this blog post, we'll explore the key differences between these two popular CSS frameworks.
#### What is Tailwind CSS?
Tailwind CSS is a utility-first CSS framework that emphasizes flexibility and customization. Unlike traditional frameworks, Tailwind doesn't come with predefined components. Instead, it provides low-level utility classes that you can mix and match to build any design.
The utility-first approach of Tailwind CSS allows developers to apply styles directly in the HTML. This leads to a more streamlined and maintainable codebase. For instance, you can easily add margins, padding, or colors by using utility classes like `mt-4`, `p-2`, or `text-blue-500`.
One of the standout features of Tailwind CSS is its high level of customization. You can configure almost every aspect of the framework through a configuration file. This allows you to create a design system that perfectly fits your project's needs. Additionally, Tailwind's JIT (Just-in-Time) mode ensures that the final CSS file includes only the styles you use, resulting in a smaller file size.
#### What is Bootstrap?
Bootstrap is a widely-used CSS framework developed by Twitter. It follows a component-based approach, providing a collection of pre-designed components and utilities to build responsive web layouts quickly. Bootstrap is known for its ease of use and extensive documentation.
Bootstrap's component-based approach offers ready-made solutions for common UI elements like buttons, forms, and navigation bars. These components are fully responsive and can be customized to match your design. This makes Bootstrap an excellent choice for rapid development.
Bootstrap's ease of use is one of its biggest advantages. With a comprehensive set of components and utilities, developers can quickly prototype and build functional websites. The extensive documentation and large community support further simplify the development process.
#### Key Differences Between Tailwind CSS and Bootstrap
##### Philosophy and Design Approach
**Utility-First vs Component-Based**
Tailwind CSS focuses on utility-first classes, giving developers granular control over the design. Bootstrap, on the other hand, offers a component-based approach, providing pre-designed components that can be easily integrated into your project.
##### Customization and Flexibility
**Configuration in Tailwind vs Predefined Styles in Bootstrap**
Tailwind CSS allows for extensive customization through its configuration file. You can define your own colors, spacing, and breakpoints. Bootstrap provides a set of predefined styles that can be customized using SASS variables, but it offers less flexibility compared to Tailwind.
##### Learning Curve
**Ease of Learning and Implementation**
Tailwind CSS has a steeper learning curve due to its utility-first approach. Developers need to familiarize themselves with various utility classes. Bootstrap is easier to learn for beginners, thanks to its intuitive component-based structure and extensive documentation.
#### Pros and Cons of Tailwind CSS
##### Pros
**Highly Customizable**
Tailwind CSS is highly customizable, allowing developers to create unique and tailored designs for their projects.
**Encourages Reuse of CSS**
The utility-first approach encourages the reuse of CSS, resulting in a more maintainable codebase.
**Smaller Final CSS File Size**
With Tailwinds JIT mode, the final CSS file includes only the styles used in the project, resulting in a smaller file size and faster load times.
**More maintainable long-term**
Tailwind CSS is more maintainable long-term due to its utility-first approach, which promotes atomic classes for specific styling, reducing the need for custom CSS and making styles predictable and easy to manage.
##### Cons
**Initial Setup and Configuration**
Setting up and configuring Tailwind CSS can be time-consuming, especially for beginners.
**Steeper Learning Curve**
The utility-first approach requires developers to learn and remember numerous utility classes, making the learning curve steeper.
#### **Pros and Cons of Bootstrap**
##### Pros
**Easy to Get Started**
Bootstrap is beginner-friendly and easy to get started with, thanks to its comprehensive documentation and pre-designed components.
**Extensive Documentation and Community Support**
Bootstrap has extensive documentation and a large community, providing ample resources and support for developers.
**Predefined Components and Templates**
Bootstrap offers a wide range of predefined components and templates, making it ideal for rapid development and prototyping.
##### Cons
**Larger File Size**
Bootstrap's comprehensive set of components and utilities results in a larger file size, which can impact page load times.
**Less Flexibility Compared to Tailwind**
While Bootstrap is customizable, it offers less flexibility compared to Tailwind CSS, making it harder to create unique designs.
#### **Use Cases for Tailwind CSS**
**Custom Design Systems**
Tailwind CSS is ideal for projects that require custom design systems, allowing developers to create unique and consistent styles.
**Projects Requiring Unique and Flexible Designs**
Tailwind's flexibility makes it suitable for projects that demand unique and adaptable designs, such as bespoke websites and applications.
#### Use Cases for Bootstrap
**Rapid Prototyping**
Bootstrap's component-based approach is perfect for rapid prototyping, enabling developers to quickly create functional and visually appealing interfaces.
**Projects Needing Standard, Pre-Built Components**
Bootstrap is ideal for projects that require standard, pre-built components, such as corporate websites and internal tools.
**Performance Considerations**
Impact on Page Load Times
The size of the CSS file can significantly impact page load times. Tailwind CSS, with its JIT mode, produces smaller CSS files, leading to faster load times. Bootstrap's larger file size can slow down page loading, especially on mobile devices.
**Optimizing for Performance**
Both frameworks offer ways to optimize performance. Tailwind's JIT mode and Bootstrap's SASS variables allow developers to include only the necessary styles, reducing the final CSS file size.
**Community and Ecosystem**
Popularity and Community Support
Bootstrap has a larger user base and community support, with extensive resources available online. Tailwind CSS is growing rapidly in popularity, with a vibrant community and increasing resources.
**Availability of Resources and Plugins**
Both frameworks have a rich ecosystem of resources and plugins. Bootstrap has a more extensive library of third-party components and templates, while Tailwind CSS offers a growing number of plugins and tools for customization.
#### Conclusion
Choosing the right CSS framework depends on your project's specific needs and your team's familiarity with the frameworks. Tailwind CSS offers unparalleled customization and flexibility, making it ideal for unique and complex projects. Bootstrap, with its ease of use and comprehensive components, is perfect for rapid development and standard designs.
Ultimately, both Tailwind CSS and Bootstrap are powerful tools in the web development arsenal.
### Optimizing JSON columns in Laravel
*Published on October 1, 2024*
---
Working with large datasets stored in JSON columns presents significant performance issues, especially when filtering and sorting. In my experience, these challenges became evident while monitoring PHP processes and managing large volumes of records, leading to execution time limits being hit.
##### Monitoring and Execution Time Issues
As part of my regular monitoring duties, I encountered max execution times of 30 seconds while querying JSON columns in a 580k record dataset. JSON columns, though flexible, are prone to performance bottlenecks, particularly without proper indexing.
##### Sorting and Filtering with JSON Columns
The first major issue appeared when working on a [Filament](https://kirschbaumdevelopment.com/insights/why-we-love-filament) list record page, which had default sorting applied to a JSON attribute. The absence of indexing on this attribute resulted in a significant slowdown, especially when processing over 10,000 records. Without an index, querying and sorting through nested JSON attributes can cause execution delays and inefficiencies in retrieving results, pushing PHP processes beyond acceptable limits.
#### Introducing Virtual Columns
When faced with performance issues from sorting and filtering large JSON columns, I revisited a previous solution: [virtual columns](https://kirschbaumdevelopment.com/insights/leveraging-virtual-generated-columns). Virtual columns in MySQL allow me to create an indexed, computed column from JSON data, making queries more efficient without duplicating data.
##### Why Virtual Columns Work Better
Unlike standard JSON columns, virtual columns are calculated automatically from existing data but can be indexed, making them faster for querying. This improves sorting and filtering performance significantly, especially in large datasets where execution time is critical.
##### How to Implement Virtual Columns
I implemented virtual columns by adding a migration that created a new indexed column for filtering and sorting. This virtual column extracted and indexed specific JSON attributes, drastically improving query performance. Here's an example migration:
```php
$table->string('approved_at')
->nullable()
->virtualAs("json_unquote(json_extract(data, '$.latest_approval_date'))");
$table->index('approved_at');
```
By indexing this virtual column, I was able to reduce query times and improve overall efficiency, especially when filtering and sorting large datasets.
#### Benchmarking the Results
Once I implemented the virtual columns, I needed to ensure the performance gains were real. Benchmarking provided concrete data, comparing the execution times of filtering, sorting, and paginating large datasets using both the original nested JSON column and the new virtual column with indexing.
##### Before: Nested JSON Columns
With over 580k records, queries on the nested JSON column were slow:
\- Sorting a page of 100 records took over 5,000ms.
\- Filtering + sorting + paginating took nearly 2,000ms.
```php
Benchmark::dd([
'count' => fn () => Document::count(),
'paginate' => fn () => Document::paginate(100),
'filter + paginate' => fn () => Document::where('data->latest_approval_date', '>', '2024-09-05')->paginate(100),
'sort + paginate' => fn () => Document::orderBy('data->latest_approval_date')->paginate(100),
'filter + sort + paginate' => fn () => Document::where('data->latest_approval_date', '>', '2024-09-05')->orderBy('data->latest_approval_date')->paginate(100),
], iterations: 100);
```
##### After: Virtual Column + Index
After indexing the virtual column, the improvements were substantial:
\- Sorting the same page of 100 records dropped to 750ms (**7.5x faster**).
\- Filtering + sorting + paginating improved to just 53ms (**36x faster**).
These benchmarks confirmed the effectiveness of virtual columns in optimizing query performance.
```php
Benchmark::dd([
'count' => fn () => Document::count(),
'paginate' => fn () => Document::paginate(100),
'filter + paginate' => fn () => Document::where('approved_at', '>', '2024-09-05')->paginate(100),
'sort + paginate' => fn () => Document::orderBy('approved_at')->paginate(100),
'filter + sort + paginate' => fn () => Document::where('approved_at', '>', '2024-09-05')->orderBy('approved_at')->paginate(100),
], iterations: 100);
```
#### Steps
##### 1. Add a Virtual Column with Migration
To improve performance, we'll start by adding a virtual column for the `approved_at` field. This column extracts and indexes the JSON attribute for better query performance.
```php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->string('approved_at')
->nullable()
->virtualAs("json_unquote(json_extract(data, '$.latest_approval_date'))");
$table->index('approved_at');
});
}
public function down(): void
{
Schema::table('documents', function (Blueprint $table) {
$table->dropColumn('approved_at');
});
}
};
```
##### 2. Create a Trait for Virtual Fields
We'll create a `HasVirtualFields` trait to ensure that virtual fields are not mistakenly saved.
```php
namespace App\Models\Concerns;
trait HasVirtualFields
{
public function save(array $options = [])
{
if (isset($this->virtualFields)) {
$this->attributes = array_diff_key($this->attributes, array_flip($this->virtualFields));
}
return parent::save($options);
}
}
```
##### 3. Add the Trait and Virtual Column Property to Your Model
In the model, include the trait and define the virtual fields. This ensures that any virtual columns are properly managed.
```php
use App\Models\Concerns\HasVirtualFields;
class Document extends Model
{
use HasVirtualFields;
protected array $virtualFields = [
'approved_at',
];
}
```
##### 4. Testing Environment
To test the performance improvements, we'll generate fake data and benchmark the queries before and after using virtual columns. Use the following provisioning script:
```php
$count = 500 * 1000;
for ($i = 0; $i < 250; $i++) {
Document::factory()->count(1000)->create();
}
```
##### 6. Wrapping Up with Unit Tests
Write tests to verify that the virtual column works as expected. Here's an example test suite:
```php
namespace Tests\Feature\Models;
use Tests\TestCase;
use App\Models\Document;
class DocumentTest extends TestCase
{
public function testApprovedAt()
{
$date = fake()->dateTimeBetween()->format(DATE_ATOM);
$document = Document::factory()->create([
'data' => [
'latest_approval_date' => $date,
],
]);
$document->refresh();
$this->assertEquals($date, $document->approved_at);
}
}
```
This complete solution ensures that your JSON columns can be optimized for performance, particularly for large datasets.
#### Conclusion and Best Practices
Using virtual columns with indexing can dramatically improve performance when working with large datasets and JSON columns. By transitioning from nested JSON queries to indexed virtual columns, I was able to reduce query times by **up to 36x**.
**Best Practices:**
\- Use virtual columns to index frequently queried JSON attributes.
\- Always benchmark before and after implementing changes to measure real performance improvements.
\- Ensure your database structure evolves with your data as it scales, especially with JSON-heavy models.
### Tailwind Transition Colors
*Published on October 24, 2024*
---
Creating stunning web interfaces is as much an art as it is a science. With Tailwind CSS, web developers can elevate their projects by mastering transition colors. This guide will walk you through everything you need to know about Tailwind Transition Colors, from basic setup to advanced techniques.
#### What Are Tailwind Transition Colors?
Tailwind Transition Colors are directives within the Tailwind CSS framework that allow developers to add smooth color transitions to their web elements. These transitions can be applied to various properties such as background colors, text colors, and border colors, enhancing the user experience with visually appealing effects.
Color transitions in Tailwind CSS make your website feel more interactive and responsive. They guide the user's eye and create a cohesive look and feel, making the web pages more engaging.
Whether you're working on a professional dashboard, a landing page, or a personal blog, understanding how to leverage Tailwind Transition Colors will set your projects apart.
#### Why Use Tailwind for Transition Colors?
Tailwind CSS is a utility-first framework that offers unparalleled flexibility and customization. Using Tailwind for transition colors provides several benefits:
- Efficiency: Tailwind’s utility classes allow you to apply styles directly in your HTML, reducing the need for separate CSS files.
- Consistency: Developers can maintain consistent design patterns throughout their projects, ensuring a uniform aesthetic.
- Customization: Tailwind is highly customizable, allowing you to define your color schemes and transitions easily.
Tailwind simplifies the process of creating complex UIs, making it easier for developers to implement and maintain transition effects. Its community and extensive documentation further support its use.
#### Setting Up Tailwind CSS
Before diving into transition colors, you need to set up Tailwind CSS in your project. Here’s a quick guide:
Install Tailwind CSS:
```bash
npm install tailwindcss
```
Create a Tailwind Configuration File:
```bash
npx tailwindcss init
```
Configure Tailwind in Your CSS File:
```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
By following these steps, you will have Tailwind CSS ready to go in your project. Tailwind's configuration file allows you to customize settings to fit your needs precisely.
#### Basic Transition Properties in Tailwind
Tailwind CSS offers a range of transition properties that you can apply out-of-the-box. Some of the essential properties include:
Transition Duration:
```html
...
```
Transition Timing Function:
```html
...
```
Transition Delay:
```html
...
```
These properties allow you to control how long a transition takes, how it progresses, and whether it delays before starting. By mastering these basics, you can begin creating smoother and more dynamic UI elements.
#### Applying Color Transitions
Applying color transitions in Tailwind is straightforward. Here's how you can apply different types of color transitions:
Background Color Transition:
```html
...
```
Text Color Transition:
```html
...
```
Border Color Transition:
```html
...
```
These simple directives can significantly enhance the visual appeal of your web elements. The Tailwind framework makes it easy to add these transitions with just a few classes.
#### Advanced Techniques and Customizations
For those looking to go beyond basic transitions, Tailwind offers advanced customization options. You can extend Tailwind’s default configuration to create more complex transitions:
Custom Transition Durations:
```javascript
// tailwind.config.js
module.exports = {
theme: {
extend: {
transitionDuration: {
'0': '0ms',
'2000': '2000ms',
},
},
},
}
```
Combining Multiple Transitions:
```html
...
```
Customizing Tailwind to fit your specific needs can help you achieve the exact look and functionality you desire. These advanced techniques can elevate your design and set your projects apart from the competition.
#### Practical Examples and Use Cases
To see Tailwind Transition Colors in action, consider the following practical examples:
Interactive Buttons:
```html
```
Card Hover Effects:
```html
...
```
Navbar Link Transitions:
```html
```
These examples demonstrate how you can use Tailwind to create engaging and interactive user interfaces. Implementing these transitions helps maintain user interest and improves overall experience.
#### Common Pitfalls and How to Avoid Them
When working with Tailwind Transition Colors, there are some common pitfalls to be aware of:
- Overusing Transitions: Overloading your website with transitions can make it feel sluggish. Use them sparingly to avoid overwhelming your users.
- Inconsistent Styles: Ensure that your transitions maintain consistency across your site to provide a seamless user experience.
- Performance Issues: Optimize your transitions to ensure they do not negatively impact the performance of your website.
By keeping these points in mind, you can avoid common mistakes and create a more polished and professional-looking site.
#### Best Practices for Smooth Transitions
To ensure your transitions are as smooth as possible, follow these best practices:
- Keep It Simple: Simplicity is key. Focus on creating clean, straightforward transitions that enhance the user experience.
- Test Across Devices: Make sure your transitions look good on all devices, including mobile phones and tablets.
- Use Hardware Acceleration: Take advantage of CSS properties that enable hardware acceleration to improve performance.
Implementing these best practices will help you create effective transitions that enhance your website's overall aesthetics and functionality.
#### Conclusion
Tailwind Transition Colors offer a powerful way to enhance the visual appeal and user experience of your web projects. By understanding and implementing these transitions, you can create more engaging and interactive web interfaces.
With the right setup, customization, and best practices, Tailwind CSS can become an invaluable tool in your web development arsenal.
### How to Make Laravel Eloquent “WHEREIN” Query. A Step-by-Step Guide
*Published on November 4, 2024*
---
When working with databases in Laravel, mastering queries can significantly impact the performance and efficiency of your applications. One powerful feature is the `WHEREIN` query in Laravel Eloquent, which allows you to retrieve records that match any value in a given array.
In this guide, we will walk you through everything you need to know about using `WHEREIN` with Laravel Eloquent, from basic usage to advanced techniques.
#### Introduction to Laravel Eloquent WHEREIN Query
Laravel Eloquent is an elegant ORM that makes database interactions intuitive and straightforward. One of its vital functionalities is the `WHEREIN` query, used when you need to fetch records that correspond to one of several values. This is particularly useful in scenarios where a simple `WHERE` clause isn't sufficient.
By the end of this guide, you'll have a comprehensive understanding of how to implement `WHEREIN` in your Laravel projects, ensuring more efficient and readable code.
#### Basic Usage of WHEREIN in Laravel Eloquent
The `WHEREIN` method can be easily added to your Eloquent queries. Let's start with a simple example to grasp its basic usage. Suppose you want to fetch users based on an array of user IDs. Here's how you can do it:
```php
$users = User::whereIn('id', [1, 2, 3])->get();
```
In this example, the `whereIn` method filters users whose IDs are either 1, 2, or 3. This method can be extremely handy for various querying needs.
**Practical Application**
Consider an ecommerce application where you need to retrieve products from specific categories. Using `whereIn`, you can easily achieve this:
```php
$products = Product::whereIn('category_id', [5, 9, 12])->get();
```
#### Advanced WHEREIN Queries
The `WHEREIN` query isn't limited to simple array matching. You can also use it in more complex scenarios, such as nested queries and subqueries.
**Nested Queries**
Nested `WHEREIN` queries can be used when you need to match records based on a subquery. For example, consider fetching users who made orders within a specific date range:
```php
$userIds = Order::whereBetween('created_at', ['2023-01-01', '2023-01-31'])->pluck('user_id');
$users = User::whereIn('id', $userIds)->get();
```
**Subqueries**
Subqueries can further simplify your code. Here’s an example of using a subquery directly within the `whereIn` method:
```php
$users = User::whereIn('id', function($query) {
$query->select('user_id')
->from('orders')
->where('created_at', '>=', now()->subMonth());
})->get();
```
#### Practical Examples
To solidify your understanding, let's look at some practical examples where `WHEREIN` could be used effectively.
**Example 1**
Fetching all posts by multiple authors:
```php
$posts = Post::whereIn('author_id', [7, 13, 19])->get();
```
**Example 2**
Retrieving orders from specific statuses:
```php
$orders = Order::whereIn('status', [OrderStatus::Pending, OrderStatus::Shipped, OrderStatus::Completed
'pending', 'shipped', 'completed'])->get();
```
These examples illustrate how versatile the `WHEREIN` method can be in different real-world scenarios.
#### Common Pitfalls and How to Avoid Them
While `WHEREIN` is powerful, it's essential to use it correctly to avoid common pitfalls that may affect the performance and accuracy of your queries.
**Pitfall 1: Large Arrays Impacting Performance:**
Avoid using very large arrays directly in your `whereIn` queries as it can lead to performance issues. Instead, consider chunking your array into smaller parts or using other query optimization strategies.
**Pitfall 2: Null Values in Arrays:**
Ensure that your arrays do not contain null values unless explicitly required. Null values can cause unexpected results when used in `whereIn` queries.
**Pitfall 3: Type Inconsistencies:**
Be cautious with type inconsistencies. The values in your array should match the column type you're querying against to avoid unexpected results or errors.
#### Best Practices for Efficient Queries
To make the most out of `WHEREIN` queries, follow these best practices for optimal performance and maintainability.
**Practice 1: Use Eloquent Collections:**
Where possible, leverage Eloquent collections for chaining and lazy loading to manage large datasets efficiently.
**Practice 2: Index Your Columns:**
Ensure that the columns you frequently use in `WHEREIN` queries are indexed. Proper indexing can significantly improve query performance.
**Practice 3: Optimize Array Sizes:**
When dealing with large datasets, break down your `WHEREIN` arrays and use chunking to process them in manageable sizes.
#### Conclusion
Mastering the `WHEREIN` query in Laravel Eloquent is a valuable skill for any web developer. It allows you to write more efficient and readable code, handle complex queries with ease, and ultimately improve the performance of your applications.
By following the tips and examples provided in this guide, you'll be well-equipped to incorporate `WHEREIN` queries into your Laravel projects effectively.
### How to set up a new Laravel project
#### Using Inertia, React and Typescript
*Published on December 5, 2024*
---
I recently started a new Laravel project, and as usual, I'm using Inertia, React, and TypeScript. It's been a while since I set up a project from scratch, so I thought I'd put together a quick guide with all the steps you need to get off the ground. If you're like me and prefer this stack, or maybe you're interested in trying it out for the first time, this should help you get started. Let's dive in!
First up, let’s get a fresh Laravel installation to work with. Create a new Laravel app using the following command:
```shell
laravel new my-app
```
Once your project is created, you’ll need to install the Inertia.js Laravel adapter. This allows your Laravel backend to handle requests and serve pages using Inertia:
```shell
composer require inertiajs/inertia-laravel
```
Next, create a root template file where Inertia will connect and manage your front-end page changes without a full browser refresh. I usually put this in `resources/views/app.blade.php`:
```html
@viteReactRefresh
@vite('resources/js/app.tsx')
@inertiaHead
@inertia
```
Now, set up the Inertia middleware to handle the requests seamlessly. This command will create the necessary middleware for you:
```shell
php artisan inertia:middleware
```
Add the newly created `HandleInertiaRequests` middleware to your project's middleware stack in `bootstrap/app.php` so it can process requests properly:
```php
use App\Http\Middleware\HandleInertiaRequests;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
HandleInertiaRequests::class,
]);
});
```
I use `nvm` to manage Node.js versions effectively. To specify the Node.js version for this project, create a `.nvmrc` file with the current LTS version. At the moment, it’s `lts/iron`:
```text
lts/iron
```
#### Setting Up Inertia with TypeScript
The next part is setting up Inertia with TypeScript. This is essential if you want type safety in your JavaScript/React code. Begin by installing necessary dependencies:
```shell
npm install @inertiajs/react react react-dom
npm install --save-dev typescript @vitejs/plugin-react @types/react @types/react-dom
```
After installing these, configure Vite, the build tool Laravel uses. Update the `vite.config.js` file like this:
```js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.tsx'],
refresh: true,
}),
react(),
],
resolve: {
alias: {
'@': '/resources/js',
},
},
});
```
Now, let’s configure TypeScript by creating a `tsconfig.json` file with these options, which set up your compiler environment for handling JavaScript and TypeScript together:
```json
{
"compilerOptions": {
"allowJs": true,
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"isolatedModules": true,
"target": "ESNext",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"paths": {
"@/*": ["./resources/js/*"]
}
},
"include": ["resources/js/**/*.ts", "resources/js/**/*.tsx", "resources/js/**/*.d.ts"]
}
```
Rename the existing `resources/js/bootstrap.js` to `resources/js/bootstrap.ts` to align with TypeScript usage.
To allow TypeScript to recognize any global variables or interfaces, create a `resources/js/types/global.d.ts` file like this:
```typescript
import { AxiosInstance } from 'axios';
declare global {
interface Window {
axios: AxiosInstance;
}
}
```
For Vite-specific references, create a `resources/js/vite-env.d.ts` with the following content:
```typescript
///
```
Rename `resources/js/app.js` to `resources/js/app.tsx` and populate it with this content to bootstrap your React Inertia app:
```tsx
import './bootstrap';
import '../css/app.css';
import { createRoot } from 'react-dom/client';
import { createInertiaApp } from '@inertiajs/react';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const pages = import.meta.glob('./Pages/**/*.tsx', { eager: true });
createInertiaApp({
title: (title) => ${title ? ${title} - : ''}${appName},
resolve: (name) => pages[`./Pages/${name}.tsx`],
setup({ el, App, props }) {
const root = createRoot(el);
root.render();
},
progress: {
color: '#4B5563',
},
});
```
Create a React component called WelcomePage in `resources/js/Pages/WelcomePage.tsx` to test that everything is working as expected:
```tsx
export default function WelcomePage() {
return (
Welcome
This is a welcome page
);
}
```
Finally, update the `routes/web.php` file so that it uses this new component when loading the homepage:
```php
Identity providers.
Add a new Identity provider, selecting “OpenID Connect” and adding GitHub as with its OIDC URL ([https://token.actions.githubusercontent.com](https://token.actions.githubusercontent.com/)). Under “Audience”, use “[sts.amazonaws.com](http://sts.amazonaws.com/)”.
##### 2. Create an IAM Role in AWS
The second step is to create an IAM Role (not user), configure a trust policy and add the necessary permissions to perform the tasks your Github action needs. At Kirschbaum, we create separate roles for different purposes, always following the principle of the least privilege, where we assign only the required permissions to each role, limiting its access and scope.
In the role creation wizard, select “Custom Trust Policy”, and in its contents, you can put the following (make sure to replace the variables).
```
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::{aws-account-id}:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:{gh-org}/{gh-repo}:*"
}
}
}
]
}
```
Next, you'll need to assign the appropriate permissions to your IAM Role (e.g. Access to an S3 bucket, to the ECS service, etc).
##### 3. Configure your GitHub Actions workflow
Now that we have the OIDC connection configured and the IAM role created, we can set up our Github action. Add a workflow file (deploy.yml) with the following:
```
name: Preview Deployment (SST)
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: {arn:aws:iam::123456789012:role/GitHubOIDC}
aws-region: us-east-1
- name: Deploy resources
run: aws s3 cp my-app/ s3://my-bucket/ --recursive
```
When this action runs, GitHub generates a short-lived OIDC token and exchanges it for AWS credentials, valid only for the duration of the workflow. This workflow is simply copying the files to S3, but you can use this method to perform actions in any AWS service and when using deployment tools like Bref or SST.
This example integrates with Github, but of course you can also integrate with other providers like GitLab or Bitbucket.
##### Key Benefits of OIDC
- **No static secrets**: Credentials are generated dynamically. If GitHub gets hacked one day and someone gets access to all existing repository secrets, you are safe.
- **Granular access control**: Using the AWS Role Trust Policy, you can restrict access to specific Github organizations, repositories, and branches.
- **Minimal maintenance**: AWS and GitHub handle the token exchange process, reducing operational overhead.
#### Conclusion
Long-lived tokens in AWS are a significant security risk, but AWS provides robust alternatives with IAM Identity Center and OIDC. By using temporary, short-lived credentials, you can significantly reduce the attack surface and maintain compliance with security standards.
Long-lived tokens in AWS pose a significant security risk, but robust alternatives like IAM Identity Center and OIDC offer secure, auditable temporary credentials. By adopting these methods, you can reduce risks, maintain compliance with security standards, and enhance operational efficiency.
If you need any help securing your AWS account or deploying Laravel apps in AWS, Kirschbaum is an AWS certified company and we would be happy to help.
### How to enhance code consistency & efficiency with Laravel Pint
*Published on February 11, 2025*
---
#### Laravel Pint
Have you ever opened up a PR only to play the game of "Find the change" through a wall of green and red diff, and to make matters worse, 99% of the changes are indentation or formatting? I think we all have.
The above scenario generally happens when somebody makes a change and their IDE tries to help, but formats a file based on *their* rules, which are different from *your* rules. Great intention, but poor results.
If you’re wondering how to end this madness, allow me to introduce you to Laravel Pint.
#### The importance of consistent code standards
In any software development project, maintaining consistent code standards is crucial for several reasons. First, it ensures that the code is readable and understandable by any developer who might work on it, reducing the likelihood of errors and making onboarding new team members more manageable. Consistency also improves collaboration, as developers can focus on solving problems, rather than deciphering different coding styles (or fighting that dreaded wall of green and red mentioned above). The basis of a great set of coding standards is not solely based on which rules you follow, but ensuring that everyone follows the same rules.
Moreover, in projects spanning multiple teams or repositories, consistent standards prevent discrepancies and maintain a unified codebase, streamlining development and deployment processes.
#### Laravel Pint: a new standard in code formatting (for Laravel)
Laravel Pint, an opinionated PHP code style fixer for Laravel projects, is designed to help developers maintain a consistent coding standard across their codebase. It extends the capabilities of PHP CS Fixer by specifically supporting Laravel's ecosystem, including Blade templates (with a plugin) - a key differentiator that can streamline the formatting process for Laravel developers.
#### Why Pint?
Laravel Pint is a streamlined, zero-config code formatting tool built on top of PHP CS Fixer but optimized for Laravel projects. Unlike PHP CS Fixer, which requires manual configuration and rule setup, Pint works out of the box with sensible defaults tailored for Laravel's coding standards, making it easier to integrate into projects. It also provides a simpler and more intuitive CLI experience, reducing setup time and maintenance. Since it's officially supported by Laravel, Pint ensures better compatibility with Laravel conventions, making it the preferred choice for developers working within the Laravel ecosystem.
#### Transitioning from CS Fixer to Laravel Pint
The transition from PHP CS Fixer to Laravel Pint is designed to be smooth and straightforward, just like other pints should be. Pint uses a zero-configuration approach by default, automatically adopting Laravel's recommended coding style. This simplicity means developers can integrate Pint into their workflow without a steep learning curve or the need to configure a myriad of options.
##### Migrating a custom CS Fixer configuration
If you've been using a custom PHP CS Fixer configuration, transitioning to Pint can still be seamless. Here's how to migrate your custom settings:
1\. **Identify rules:** Start by identifying the rules you've customized in your PHP CS Fixer configuration (.`php-cs-fixer.php`). Map these rules to Pint's configuration style.
2\. **Create Pint configuration:** Laravel Pint uses a `.pint.json` configuration file. You can create this file in your project's root directory and translate your custom rules from CS Fixer to Pint. While Pint aims to be zero-configuration, it does support customization for specific rules, and since it's built on top of PHP CS Fixer, all of your rules should work. While you're in here, this is a great time to evaluate your rules. Michele Locati's [php-cs-fixer-configurator](https://mlocati.github.io/php-cs-fixer-configurator) is a pretty awesome tool for this.
Example:
```javascript
{
"preset": "laravel",
"rules": {
"array_syntax": {"syntax": "short"},
"binary_operator_spaces": {"default": "align_single_space"}
}
}
```
3\. Test and adjust: Run Pint on your codebase and compare the results with your previous CS Fixer outputs. Adjust the rules as necessary to achieve the desired formatting.
##### Setting Pint up to manage Blade files
Out of the box, Blade files are not supported. But this is really easy to solve. Stillat have released their [Blade parser](https://stillat.com/blade-parser). This tool is fantastic, as it helps check Blade syntax and any PHP within the Blade templates can easily be evaluated through your Pint rules. An added benefit of this parser is it can work with Prettier to format your Blade files through Prettier which can format your frontend CSS and JavaScript assets at the same time for formatted and consistent code-style everywhere.
Details on setting up the parser to work with Blade is available here:
##### Running Pint
Now that you have Pint set up just the way you (or your team) likes it, it’s time to gain some real advantages.
The barebones run can be done with a simple:
```
./vendor/bin/pint
```
This will check your files and complain about anything it finds a little naughty in there. If you need some more detail you can throw a `-v` on the end there to get a more verbose output.
Other options that are super useful are:
- `--test`: to view issues but not act on them
- `--dirty`: to only fix issues on the uncommitted files
- `--repair`: to fix the issues but still exit out with a non-zero code (this is particularly useful for actions and workflows)
***NB****: With any script that modifies code: check your code after this has run. There's no benefit in having beautifully formatted code that doesn't work. I mean obviously your 100% code-coverage test suite will catch this, but check it yourself, you know, just in case.*
##### Setting up Pint in pre-commit workflows
Integrating Pint into your pre-commit workflow is a critical step to ensure that code style consistency is maintained across your project. Just as a pint is meant to be enjoyed, not kept on the bar, Laravel Pint isn't super useful if we install it and leave it. Here's a simple way to set this up using Git hooks:
1\. **Install Pint:** First, install Laravel Pint via Composer. If you've followed along up till here, this is done already…
```
composer require laravel/pint --dev
```
2\. **Create a Git Hook:** Set up a pre-commit Git hook to run Pint automatically. In your project's `.git/hooks` directory, create a new file named `pre-commit` and add the following script:
```
#!/bin/sh
./vendor/bin/pint --dirty --test
if [ $? -ne 0 ]; then
echo "Code style issues found! Please fix them before committing."
exit 1
fi
```
3\. **Make the hook executable:** Ensure the script is executable by running:
```
chmod +x .git/hooks/pre-commit
```
This setup will automatically check for code style issues using Pint before each commit, ensuring that only properly formatted code is committed to the repository.
##### Adding a GitHub Action for Laravel Pint
To enforce code style checks on pull requests and ensure consistency across contributions, you can set up a GitHub Action to run Pint. Here's a basic configuration:
1\. **Create GitHub workflow:** In your repository, navigate to `.github/workflows` and create a file named `pint.yml`.
2\. **Define the action:** Add the following configuration:
```
name: Laravel Pint
on:
pull_request:
push:
branches:
- main
jobs:
pint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4' # Replace with your PHP version
- name: Install dependencies
run: composer install --no-progress --prefer-dist --no-suggest
- name: Run Laravel Pint
run: ./vendor/bin/pint --test
```
This action will run Laravel Pint on every push or pull request, ensuring that your code adheres to the defined style rules.
You will notice that this is set to run with `--test`, this is so that it can highlight any errors but not take action, just in case it's looking to make a change you may not want.
##### Bringing a Codebase Up to Speed
For projects that have not consistently adhered to a code standard, the task of bringing the entire codebase up to speed can be daunting. Here are some valuable tips to help streamline this process:
1\. **Start small:** Begin by applying Pint to a single directory or set of files. This approach makes it easier to manage and review changes incrementally.
2\. **Automated fixes:** Pint will automatically correct style violations. This feature can save a significant amount of time, especially for larger codebases.
3\. **Review and refactor:** After automatic fixes, manually review the changes to ensure they don't inadvertently alter the logic. It's also a good opportunity to refactor any messy code.
4\. **Gradual enforcement:** Implement Pint in your CI/CD pipeline and set it to fail builds if the code does not meet the required standards. This practice enforces code style adherence gradually, giving the team time to adapt.
5\. **Team training and documentation:** Educate your team on the importance of code consistency and how to use Pint. Provide clear documentation and examples to help developers understand and adopt the new standards.
I have a preference for a quick code-freeze, formatting is implemented and checked, then work carries on as usual. It’s better to have the whole project brought up together than have to deal with formatting diffs in feature PRs.
#### Conclusion
Migrating from PHP CS Fixer to Laravel Pint offers a straightforward path to improving code consistency, especially for Laravel projects that utilize Blade templates. By integrating Pint into your pre-commit workflows and GitHub Actions, and following a structured approach to reformatting your codebase, you can achieve a cleaner, more maintainable codebase. The benefits of this transition are clear: better readability, easier collaboration, and a unified coding standard that can be enforced across multiple projects. Embracing Laravel Pint is a step toward more efficient and harmonious development practices.
The only challenge our team still faces in this process is getting everyone to consistently refer to it as "Pint" (like the measurement) or "Pint" (rhyming with "mint").
PS. It's supposed to be pronounced to rhyme with "mint"...
### An advanced guide to Laravel Sail
*Published on February 19, 2025*
---
Laravel Sail is a great tool for developing Laravel applications without having to set up services on your local machine. It also helps you avoid issues in your application caused by differences between your development and production environments.
#### Sail is NOT for production use
A very common question in various Laravel discussion boards is how a developer can set up their Sail application on a production server. Sail is not meant to be used in a production environment. It is strictly a development tool. There are many good reasons why you shouldn't use Sail in a production environment but they usually boil down to these three reasons:
- The sail containers are bloated. Because Sail needs to support every service generally supported by Laravel, the containers include the PHP extensions for MariaDB, MySQL, PostgreSQL, and MongoDB. Very few projects I've worked on have used all of these database engines at the same time. This makes the containers (relatively) slow compared to what you can set up for a production environment.
- The Sail containers run on the PHP development server. This means that only a single HTTP request can be processed at a time. A production application hopefully gets more traffic than that.
- Security. Being a development container, there is very little hardening done to prevent a malicious actor from gaining root access to your container (and by extension your application, database, etc).
#### Sail basics
If you don't have PHP installed locally (and you don't want to install it) please read The "Sail paradox" section. Eventually I would recommend installing PHP locally for convenience as you develop your application.
Here are the essential commands for your project:
##### Installing
```sh
php artisan sail:install # link to installing docs
php artisan sail:add # link to adding docs
php artisan sail:publish # publish the sail files for customization
```
##### Adding a shell alias
```sh
alias sail='sh $([ -f sail ] && echo sail || echo vendor/bin/sail)'
```
#### "Hidden" features of Laravel Sail
##### Container port forwarding
For most Sail services there are `FORWARD_*` environment variables you can define to modify the local ports your containers are bound to. This is useful if you have services already bound to common ports like 80, 3306, 1025, 8025, etc.
##### Advanced environment variables
There are also a number of environment variables you can set:
```sh
SUPERVISOR_PHP_COMMAND # allows you to customize the start container command (used when adding laravel octane to sail)
SUPERVISOR_PHP_USER # allows you to customize the laravel.test container supervisor user
APP_SERVICE # allows you to change the name of the laravel.test container in the docker-compose.yml file
# to match the identifier of the local user (this keeps file permissions properly matched between the local machine and the container)
WWWUSER # sets the user identifier of the sail user
WWWGROUP # sets the group identifier of the sail user
SAIL_FILES # allows you to define additional docker-compose files to be merged into the main configuration
```
#### The "Sail paradox"
Since Sail allows you to develop Laravel applications in containerized environments, you don't actually need PHP or Composer installed locally on your development machine. However, many developers just getting started with Sail run into this scenario:
1\. Clone or create a Laravel project with Sail set up
2\. Run `sail up` (or `vendor/bin/sail up` if you have not configured a shell alias)
3\. You get the error: `sh: 0: cannot open vendor/bin/sail: No such file`
The `sail` script is only available after the project's Composer dependencies have been installed. Composer requires PHP to run. Neither tool is installed locally because you want to run your Laravel project in a container. You can't run your Laravel project in a container without starting Sail. The `sail` script is only available after the project's Composer dependencies have been installed. Composer requires PHP to run. Neither tool is installed ........... `error: Xdebug has detected a possible infinite loop, and aborted your script.`
Luckily the Laravel team has provided a solution for this problem so that you can install your Composer dependencies without having to install PHP and composer locally with this command:
```sh
docker run --rm \
-u "$(id -u):$(id -g)" \
-v "$(pwd):/var/www/html" \
-w /var/www/html \
laravelsail/php84-composer:latest \
composer install --ignore-platform-reqs
```
You can also replace `laravelsail/phpXX-composer` with any PHP version you'd like (81, 82, 83, etc).
Aside: If you are working with enough Laravel applications, I do recommend eventually installing PHP and Composer locally. With Docker and Sail, it's not required, but it does make certain aspects of your development experience smoother.
#### Advanced customization
##### Adding a docker-compose.override.yml file
There are situations where you may want to modify your personal Sail configuration without changing the configuration for the main project. To do this you can utilize the [merge](https://docs.docker.com/reference/compose-file/merge/) features of Docker compose files.
By default, Docker will look for a `docker-compose.override.yml` in the same directory as the `docker-compose.yml` file and merge it into your configuration. If you add this file to the project’s `.gitignore` file, you can modify your local Sail configuration however you like without any of your changes being added to source control.
Two YAML tags that are very important to understand when merging compose files are the `!override` and `!reset` tags. The `!override` tag allows you to replace an attribute in the compose file and the `!reset` tag can be used to remove an attribute.
For example, if the application you are working on includes the mailpit service, and you don't want to utilize it locally, you can add the following to your `docker-compose.override.yml` to disable it:
```
services:
laravel.test:
depends_on: !override # removes all dependencies of the laravel.test container except mysql
- mysql
mailpit: !reset [] # disables the mailpit service
```
##### Running multiple sail applications concurrently
If you would like to be able to run multiple Sail applications at the same time and have traffic routed through different local domains, you can set up a primary webserver that listens on ports 80 and 443 and reverse proxies the request to your sail applications on different domains.
If you set up Traefik with the Docker provider enabled, this is very easy to use with Laravel Sail. First, create a empty directory called `traefik` on your local machine and add the following to a `docker-compose.yml` file in that directory:
Traefik Docker Compose file config:
```
name: traefik
services:
traefik:
image: traefik:3
container_name: "traefik"
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
restart: unless-stopped
networks:
- traefik
networks:
traefik:
external: true
```
Before starting this Traefik container, make sure you create the Traefik Docker network by running `docker network create traefik`
Next, run `docker compose up -d` to start this container. This container will always turn on when Docker does. If the container does not automatically start for some reason, you can always run `docker compose up -d` again from your `traefik` directory.
In your Sail project, create a `docker-compose.override.yml` with the following content:
```
services:
laravel.test:
networks:
- traefik
ports: !override
- "${VITE_PORT:-5173}:${VITE_PORT:-5173}"
labels:
- "traefik.enable=true"
- "traefik.http.routers.{site}-laravel-test.rule=Host{domain}.localhost)"
- "traefik.http.routers.{site}-laravel-test.entrypoints=web"
- "traefik.http.services.{site}-laravel-test.loadbalancer.server.port=80"
- "traefik.docker.network=traefik"
networks:
traefik:
external: true
```
\- Replace `{site}` with a unique string identifier for this project (such as `myapp`). This identifier should not be the same as any other identifier on your local machine.
\- Replace `{domain}` with whatever you'd like your local domain to be.
By overriding the ports in your `docker-compose.override.yml` file, you make it so that your application does not collide with the Traefik ports and you make it so that the Sail application is now only accessible through the Traefik reverse proxy.
After you have configured your override file start your containers with `sail up -d`.
If you replaced `{domain}` with `myapp` then your application should now be accessible at `http://myapp.localhost`.
### A practical guide to mutation testing with Pest
*Published on March 5, 2025*
---
Hey fellow Laravel devs! Let's talk about something that can seriously boost the way we test our code: mutation testing. We all want clean, robust code, and we put in a lot of effort to write tests. How sure are we that our tests are actually catching everything? I mean, we've all seen tests that pass but maybe aren't as thorough as we thought. That's where mutation testing comes in.
Mutation testing goes beyond code coverage to check the quality of our tests. It works by making small changes (mutations) to our code and then rerunning our tests to see if they fail. If a test still passes with a mutation, it means the test isn't really covering that specific part of the code. Think of it like a stress test for your tests. It’s not about finding bugs in your code directly, but about highlighting areas where your tests might be a bit weak. It helps us find blind spots and make our tests more robust and reliable.
We all know that 100% code coverage doesn't guarantee perfect tests. You might have lines of code that are run by tests, but that doesn't mean you're testing every case. Mutation testing ensures that our tests are actually checking the right things. This is super important for larger projects. By using this method, we can make sure that our tests are doing their job and that they're ready to catch any issues we might accidentally introduce in the future.
So, let's dive in and see how we can use mutation testing to write better, more reliable tests!
#### What is mutation testing?
Mutation testing is a cool technique that helps us evaluate how effective our test suites are by introducing small, deliberate changes to our codebase. These changes, called mutations, are like mini-simulations of common coding errors or edge cases. The idea is that if our tests are well-written, they should detect these mutations and fail.
When we do mutation testing, the testing tool will:
- **Introduce mutations:** The tool automatically makes small changes in the code, such as modifying return values, altering method calls, or changing method arguments.
- **Rerun tests:** After each mutation, the test suite is re-executed.
- **Analyze results:** If a test fails after a mutation, it means the test has "caught" the change, which is good. If a test passes even with the mutation, it shows a weakness in our test suite.
It's important to remember that mutation testing isn't about finding bugs in our application code. It's about finding gaps in our test suite. It helps us identify areas where our tests might not be thorough enough.
#### Why should we care about mutation testing?
##### The scenario: shipping cost calculation with a hidden flaw
Imagine a function to calculate shipping costs based on order weight and premium membership status. The logic is:
- **Base cost:** Shipping starts at $10.00.
- **Weight surcharge:** For every kilogram over 5kg, an additional $2.50 is added.
- **Premium discount:** Premium members receive a 10% discount. This discount should only apply if the order weight is greater than 2kg to prevent abuse on very light, already cheap shipments.
Here's the PHP code with a tiny, but significant, bug:
```php
declare(strict_types=1);
function calculateShippingCost(float $weightInKilograms, bool $isPremiumMember): float
{
$shippingCost = 10.00; // Base shipping cost
if ($weightInKilograms > 5) {
$shippingCost += ($weightInKilograms - 5) * 2.50; // Additional cost per kg over 5kg
}
// Premium members get a discount, but only if weight is over 2kg
if ($isPremiumMember && $weightInKilograms >= 2) { // Subtle bug: Should be > 2, not >= 2 for discount to apply correctly
$shippingCost *= 0.90; // 10% discount for premium members
}
return $shippingCost;
}
```
##### The "invisible issue": a sneaky off-by-one error
The bug is in the premium discount condition: `if ($weightInKilograms >= 2)`. It should be `if ($weightInKilograms > 2)`. This >= operator means orders weighing exactly 2kg get the discount, which might be unintended. It's a subtle edge case, easy to overlook.
##### Pest test suite (initial, potentially flawed tests)
Let's create a Pest test suite that, while seemingly covering the function, might miss this subtle bug.
```php
use function calculateShippingCost;
it('calculates base shipping cost for light orders', function () {
expect(calculateShippingCost(1, false))->toBe(10.00);
expect(calculateShippingCost(4.9, false))->toBe(10.00);
});
it('adds weight surcharge for heavier orders', function () {
expect(calculateShippingCost(6, false))->toBe(12.50); // 10 + (1 * 2.50)
expect(calculateShippingCost(10, false))->toBe(22.50); // 10 + (5 * 2.50)
});
it('applies premium discount for members on orders over 2kg', function () {
expect(calculateShippingCost(2.1, true))->toBe(9.00); // 12.50 * 0.90
expect(calculateShippingCost(6, true))->toBe(11.25); // 12.50 * 0.90
});
it('does not apply premium discount for members on very light orders', function () {
expect(calculateShippingCost(1.9, true))->toBe(10.00); // No discount
expect(calculateShippingCost(0.5, true))->toBe(10.00); // No discount
});
it('handles zero weight', function () {
expect(calculateShippingCost(0, false))->toBe(10.00);
expect(calculateShippingCost(0, true))->toBe(10.00); // No discount on zero weight
});
```
##### Why standard tests might miss the issue:
Notice that the test suite includes cases for:
- Base shipping cost.
- Weight surcharge.
- Premium discount for orders over 2kg.
- No premium discount for very light orders (under 2kg).
- Zero weight orders.
However, it lacks a specific test case for an order weighing exactly 2kg for a premium member. The test suite makes assumptions about "over 2kg" but doesn't explicitly check the boundary condition at 2kg. Therefore, these tests will all pass even with the bug (>= 2 instead of > 2).
##### How mutation testing catches the invisible issue:
Now, let's introduce mutation testing using Infection PHP. Infection will automatically modify your code in small ways ("mutations") to see if your tests can "kill" these mutants (i.e., cause a test to fail).
When Infection runs, it might create a mutant by changing the >= operator to > in the premium discount condition:
##### Mutated code (hypothetical mutation by infection):
```
if ($isPremiumMember && $weightInKilograms > 2) { // Mutation: >= changed to >
$shippingCost *= 0.90;
}
```
With this mutation, if we run the same Pest test suite, the test `it('applies premium discount for members on orders over 2kg', function () { ... });` will still pass because it's testing weights above 2kg (like 2.1kg and 6kg).
However, mutation testing tools often provide ways to analyze "survived" mutants – mutants that were not killed by the existing test suite. Infection would likely report this mutant as surviving.
To specifically target this potential issue, we can add a new Pest test case that focuses on the 2kg boundary:
##### Enhanced Pest test suite (adding the crucial test):
```php
use function calculateShippingCost;
// ... (previous tests remain) ...
it('does NOT apply premium discount for members on orders exactly 2kg (boundary test)', function () {
expect(calculateShippingCost(2, true))->toBe(10.00); // Should be base cost, no discount
});
```
Now, if we run the original code with this new test, this test will fail because `calculateShippingCost(2, true)` currently returns 9.00 (due to the >= 2 bug), but the test expects 10.00.
If we run mutation testing again after adding this test, Infection will now likely kill the mutant where `>=` was changed to `>`. This is because the new test case specifically targets the behavior at the 2kg boundary.
#### Mutation testing vs. code coverage
**Code coverage** tells you if your code is being executed by tests, while **mutation testing** tells you how well your code is being tested. You might have 100% code coverage, but if your tests aren't checking the right things, they won't catch mutations.
Here's a breakdown of the key differences:
##### Code coverage:
- Measures the percentage of your code that is executed by tests.
- Indicates which parts of your code are "touched" by your tests.
- Aims to ensure that all lines of code are executed at least once during testing.
- Can be a good starting point, but **high code coverage doesn't guarantee robust tests.**
- It can miss edge cases or logical errors that are not caught by simple execution.
##### Mutation testing:
- Evaluates the effectiveness of your tests by introducing small changes to the code.
- Checks if the tests can detect the changes introduced by mutations.
- Aims to ensure the tests are asserted against specific conditions or data.
- Identifies areas where tests may be too superficial or not comprehensive.
- **A high mutation score indicates a more thorough test suite** that is less likely to miss regressions.
While hitting 100% mutation testing score may not always be necessary or possible, it's a good goal to aim for. Focusing on improving your mutation score will help you find gaps in your test suite and write more effective tests.
#### Getting started with mutation testing
Now that we know what mutation testing is and how it's different from code coverage, let's see how to use it in our Laravel projects with Pest. Pest is a testing framework for PHP that has mutation testing built in.
Before you start, make sure you have **Xdebug 3.0+ or PCOV** installed and configured. These tools are necessary for Pest to do mutation testing. If you use Laravel Herd, check here to learn how to set up Xdebug.
To get started with mutation testing in Pest, follow these steps:
- **Specify which parts of your code should be covered by your tests**. In your test file, you can use the covers() or mutates() functions to specify the classes or methods that your tests cover. For example, if you want to use mutation testing, you can add covers(...) or mutates(...) at the beginning of your test file. Both functions are identical for mutation testing purposes; however, the covers() function will also filter the code coverage report.
```
//,,,
covers(ProjectController::class, StoreProjectRequest::class, UpdateProjectRequest::class);
describe('ProjectController store', function () {
it('should create projects enabled by default', function () {
$user = User::factory()->create();
$this->actingAs($user)
->post(route('projects.store'), [
'name' => 'My project',
'topic' => 'My topic',
'description' => 'My description',
'urls' => ['https://example.com'],
'cron_expression' => '0 0 * * *',
])
->assertRedirect(route('dashboard'));
$project = $user->projects()->first();
expect($project)
->name->toBe('My project')
->topic->toBe('My topic')
->description->toBe('My description')
->urls->toBe(['https://example.com'])
->cron_expression->toBe('0 0 * * *')
->enabled->toBeTrue();
});
it('should redirect to login if user is not authenticated', function () {
$response = $this->post(route('projects.store'));
$response->assertRedirect(route('login'));
});
it('validates name is required', function () {
$user = User::factory()->create();
$this->actingAs($user)
->post(route('projects.store'), [
'topic' => 'My topic',
'description' => 'My description',
'urls' => ['https://example.com'],
'cron_expression' => '0 0 * * *',
])
->assertSessionHasErrors(['name' => 'The name field is required.']);
});
```
Notice how we use `covers(ProjectController::class, StoreProjectRequest::class, UpdateProjectRequest::class)`. This will run mutations in all these files so we can make sure the whole workflow is fully covered.
You can see more examples on ProjectController tests.
- **Run Pest with the** `--mutate` **option.** This command will start mutation testing. It’s recommended to use the `--parallel` option to speed things up by running tests in parallel:
- Pest will then re-run your tests against mutated code. If a test passes with a mutation, it means that the test is not covering that specific part of the code, and Pest will output the mutation and the diff of the code.
- **Analyze the results.** Pest will output information about:
- **Tested mutations:** These are mutations that were detected by your test suite, meaning that your tests were able to catch the changes introduced by the mutation.
- **Untested mutations:** These are mutations that were not detected by your test suite. This means that the test was not able to catch the change, indicating a gap in the tests.
- **Mutation score:** This is a percentage that indicates the quality of your test suite. A score of 100% means that all mutations were "tested," which is the goal of mutation testing.
- **Improve your tests.** If you find untested mutations or a low mutation score, you'll need to write additional or better tests to cover the uncovered code or edge cases. After you've improved your tests, rerun Pest with the --mutate option to confirm that the mutations are now tested and that your mutation score has improved.
##### Key points to remember:
- Pest will only run the tests covering the mutated code to speed up the process.
- Pest caches mutations to speed up subsequent runs.
- You can use parallel execution to run multiple tests to further speed up the process.
- **A higher mutation score means a better test suite.** A score below 100% typically means that you have missing tests or that your tests are not covering all the edge cases.
By following these steps, you can start using mutation testing to find weaknesses in your test suite and improve the quality of your tests.
#### Options & modifiers
Pest's mutation testing has a bunch of options and modifiers to fine-tune the process. These options let you customize how mutations are generated, which tests are run, and how the results are handled. Here are some of the most important ones:
- @pest-mutate-ignore: This modifier lets you ignore specific lines of code when generating mutations. This is helpful when you have code that shouldn't be mutated. To use it, just add the comment `// @pest-mutate-ignore` on the line you want to ignore.
```
public function rules(): array
{
return [
'name' => 'required',
'email' => 'required|email', // @pest-mutate-ignore
];
}
```
- `--covered-only`: This option restricts mutations to only the lines of code that are covered by your tests. This option can speed up the mutation testing process, as it will only target the parts of the code that are executed by your tests.
- `--bail`: This option stops mutation testing as soon as an untested or uncovered mutation is detected. This option can be helpful for quickly identifying issues and speeding up the feedback loop.
- `--class`: This option allows you to generate mutations for a specific class or classes. For example, if you only want to run mutation testing on the App\\Models namespace you can specify --class=App\\Models.
- `--ignore`: This option ignores mutations in the specified class or classes. For example, if you want to skip mutations in the App\\Http\\Requests namespace you can specify --ignore=App\\Http\\Requests.
- `--stop-on-uncovered`: This option stops mutation testing as soon as an uncovered mutation is detected. This is similar to the --bail option but will only stop on uncovered mutations and not untested mutations.
- `--stop-on-untested`: This option stops mutation testing as soon as an untested mutation is detected.
Check the full list of options on the Options & Modifiers docs.
If you want to learn more about how to set up and write mutation tests, take a look at this project on Github where you can find real life examples of tests.
Mutation testing helps you write better tests by revealing areas of your code that are not adequately covered or have edge cases you may have missed. By understanding the difference between tested and untested mutations, and utilizing the various options and modifiers available, you can significantly improve the robustness and reliability of your code.
### Crafting effective prompts for AI assistants
#### Part 1
*Published on March 17, 2025*
---
#### Why instructions matter
Imagine handing a critical task to a junior developer with only a half-baked spec. Chances are, you’ll get something, but not what you envisioned. Large Language Models (LLMs) operate similarly – they break down your input into tokens (words or subwords) and then predict a response token by token based on those instructions. In other words, an LLM (even a top-tier model like GPT-4o, o1, etc.) will do exactly what you ask, not necessarily what you mean. If your prompt is vague or ambiguous, the model might fill in the gaps in unpredictable ways.
Think of writing prompts like giving instructions to an intern who’s doing a task for the first time. You wouldn’t tell an intern “Make this better” without context; you’d spell out the requirements. Likewise, clear and structured prompts prevent misunderstandings. Ambiguity is the enemy: unclear directions can lead to irrelevant or inconsistent outputs. By providing precise details (just as you would in a well-written Jira ticket or API contract), you guide the AI to produce results closer to what you want. The bottom line: the effort you put into crafting good instructions directly translates into the quality of the AI’s response.
#### The PROMPT
To remember the best practices of prompt-writing, let’s use a cheeky developer-oriented acronym: PROMPT. Each letter highlights a principle to level up your prompt engineering game. Think of it as writing good code – clear, purposeful, and easy to follow.
• **P – Purpose (Plan your Prompt):** Start with a clear goal in mind. State exactly what you want the AI to do. A prompt without a defined purpose is like a function with no spec – the outcome will be anyone’s guess. For example, instead of saying “Help with Laravel middleware”, specify “Explain how to implement custom middleware in Laravel”. Clear intent sets the stage and reduces guesswork. As OpenAI’s guidelines put it, be specific and detailed about what you need – context, outcome, format, etc.. Defining the purpose up front focuses the model, much like a good unit test defines what success looks like.
• **R – Role or context:** Provide any relevant context or ask the model to assume a role. LLMs love context – it’s their equivalent of having the right configuration before running a program. If you want a particular perspective or expertise, say so! For instance, “You are a senior Laravel developer reviewing a pull request” sets a tone and expertise level. Or include actual context: “Given the following controller code \[…\]”. Supplying background details helps the model understand your question better. Context is everything for complex queries; just as a snippet of code might behave differently without the rest of the codebase, an AI’s answer improves when it knows the scenario or data it should consider.
• **O – Outcome and Output format:** Describe the desired output and format. Do you want a PHP code snippet, a step-by-step explanation, or just a one-line answer? Specify that. If you expect a list of bullet points or a JSON structure, mention it explicitly. For example: “List three optimization tips for Eloquent queries in bullet points.” This is akin to defining a function’s return type. By articulating the output expectations, you make it easier for the AI to deliver something you can use straight away. Models respond well when you show or tell them the format you need – it removes another layer of ambiguity. Leverage dedicated structured output schemas on providers that support them.
• **M – Minimize ambiguity (Maximize clarity):** Use precise language and avoid open-ended or subjective terms. If a term could be interpreted in multiple ways, clarify it. Saying “optimize this Laravel controller” is less clear than “refactor this Eloquent query for fewer database calls”. Aim to eliminate wiggle room in interpretation, much like you’d eliminate undefined behavior in code. Instead of writing “the response should be short”, define “short” precisely (e.g. “under 5 sentences”). In prompt engineering, specific beats vague every time. A good practice is to replace imprecise adjectives with concrete metrics or descriptions (e.g., “Use a 3-5 sentence paragraph to describe X” rather than “keep it fairly short”).
• **P – Provide examples (if needed):** For complex tasks, examples can guide the model. This is the “show, don’t tell” principle. If you want the AI to output code in a certain style or format, give it a small example to mimic. For instance, prompting: “Translate the following array to JSON. Example: Input: \[‘a’ => 1, ‘b’ => 2\]; Output: {"a":1,"b":2}. Now convert this array: \[…\]” gives a clear pattern to follow. Examples act as hints of the desired outcome and can dramatically improve reliability and consistency of the output. This few-shot approach is like providing unit test examples – the model sees the pattern and reproduces it. Just be sure your examples are correct and relevant, as the AI will generalize from them.
• **T – Test and Tweak:** Don’t expect a perfect answer on the first try. Just as you rarely write a bug-free feature in one go, prompt engineering is an iterative process. Try your prompt, review the output, and refine it. If the answer was off-base, ask yourself: what part of my instruction was misunderstood or too vague? Maybe you forgot to mention a crucial detail (the same way a missing env variable can break an app). Treat your prompt like code under development: debug it by adjusting phrasing, adding context, or splitting one big prompt into smaller pieces. The key is to experiment and iterate – change one thing at a time and see how the output improves. Over time, you’ll learn how even small wording changes can significantly influence the results. (More on this in the “Iterating on Prompts” section below.)
#### Understanding LLM workloads
Not all prompts are created equal – they depend on the job you’re asking the AI to do. LLMs are amazingly versatile (they can churn out prose, write code, summarize text, answer questions, and more), but each of these workloads may require a different prompting strategy. As an experienced developer, you instinctively adjust your approach for different tasks; similarly, you should adjust how you prompt for different AI tasks:
• **Free-form text generation:** This is when you want the model to produce a narrative, explanation, or any open-ended content (like an error explanation or a blog paragraph). Here, providing a guiding context or style can help. For example, “Explain in one paragraph why using Laravel’s Eloquent ORM can be beneficial, in a casual tone.” You might give the AI freedom to be creative, but you still set boundaries (topic, length, tone). If you leave it too open (“Tell me about Laravel”), you’ll get a very generic answer. So, frame your prompt to narrow down the topic and style. Essentially, **be the product owner for the content:** set acceptance criteria (topic, tone, key points) so the AI delivers something on-target.
• **Code generation and completion:** Using AI to get code snippets or help with programming requires precision in prompts. When asking for code, specify the language and context, and any specific libraries or frameworks. For instance: “In PHP/Laravel, write a middleware that logs the request method and URL.” By stating PHP/Laravel, you cue the model into the right ecosystem (so it doesn’t give you Node.js or Python code). If you have a piece of code that needs completing or debugging, include it. For example, “Here’s a Laravel query builder snippet \[…\]. Complete it to eager load related comments.” LLMs can indeed write and even debug code, but they perform best when you describe the problem clearly (just like writing a good bug report). Also, remember to double-check AI-generated code – treat it like code from a junior dev: useful, but possibly needing review.
• **Summarization and explanation:** Sometimes you’ll use LLMs to summarize long texts (logs, documentation, or even a Slack conversation) or to explain a piece of code. In these cases, the prompt should clearly separate the instruction from the content. For example: “Summarize the following error log in one sentence: .” or “Explain what the following Laravel code does, step by step: .” Use delimiters like triple quotes or XML tags to enclose the text if you’re using an API, so the model knows what it needs to summarize versus what is just instruction. The strategy here is to be straightforward: tell the model it’s summarizing or explaining, provide the text, and specify any focus for the summary (e.g., “focusing on causes of the error”). Because the model only knows what you feed in the prompt (plus its training data), ensure the critical content is included. A well-structured summarization prompt will yield a more focused and accurate summary than a generic “TL;DR please.”
• **Retrieval or Q&A (knowledge extraction):** When you want factual answers, especially about domain-specific knowledge (say details from Laravel’s docs or your project’s readme), you need to ensure the model has access to that info. By default, an LLM like GPT-4o has a lot of general knowledge but might not know specifics of your project. If the information is small, include it in the prompt: “Using the Laravel documentation below, answer the question… … Question: How do you define a route that handles POST requests?”. This way, the model retrieves from the given text rather than guessing. If the info is too large to include, that’s when techniques like RAG (discussed later) come in – effectively, you first fetch relevant data then ask the question. The prompting strategy for retrieval-style tasks is to ground the AI in provided text. Always instruct the model to base its answer only on the given context to minimize hallucinations. It’s similar to how you’d cite sources in an essay – give the AI the source material up front.
• **Chatbot or multi-turn conversations:** Interacting with an AI in a conversational manner (like a chatbot that remembers context) adds another twist to prompting. In a chat setting, you typically have system messages or an initial prompt setting the stage (e.g., “You are an AI assistant that helps with programming questions.”), and then each user message and assistant response builds on the last. Prompt design here includes maintaining context across turns and possibly reminding the model of important information if the conversation gets long. For example, if in round 1 you provided a code snippet and by round 5 you’re asking a follow-up, the model might not see the snippet unless the system or you include it again (depending on how the conversation memory is handled). The key is to keep each prompt in the conversation contextually complete enough. Sometimes rephrasing the user’s question with context in your prompt can help. Also, instruct the AI if needed to stay in character or follow a style throughout the chat. Essentially, you’re writing not just one prompt but a series of prompts (a dialogue) – each turn should be clear and build logically on the previous. This is akin to an ongoing function call that retains state; you ensure the state (context) persists so the AI doesn’t lose the thread.
In short, identify the type of task and adjust your prompt like you’d choose the right algorithm for a job. If it’s creative, give it freedom (with guidance); if it’s precise (like code or factual answers), give it structure and context. The more the prompt fits the workload, the better the output.
#### Iterating on prompts
No developer expects to write a perfect feature on the first try – we debug, we refine. Treat prompt crafting the same way. The first response you get from the AI is like a first unit test run: if it fails (or isn’t quite what you wanted), dig into why and iterate.
**Start simple and build up.** Begin with a basic prompt to see how the model responds. Think of it as your introduction for the task. If the output isn’t what you need, analyze it like a failing test. For example, if you asked for a Laravel validation rule snippet and the AI’s answer is incorrect or off-topic, check your prompt: Did you specify the Laravel version or context? Did you actually ask for a code snippet? Maybe you got a verbose explanation instead of code because you didn’t explicitly say “provide code”. This analysis is akin to checking if your API call payload is correct when you get a weird response.
**Use a debugging mindset.** When a prompt “bug” occurs (i.e., the AI’s answer is wrong or odd), isolate the issue. Is the instruction unclear or too broad? Try tightening it. Is the answer incomplete? Perhaps ask the AI to be more detailed or to format the answer differently. One change at a time – like altering one parameter in a function – will let you observe its effect. For instance, if the AI’s recipe output is missing steps, you might add “list all steps without skipping any.” If the tone is off, you could append “respond in a formal tone.” After each tweak, run the prompt again and see if the result moves closer to your desired outcome.
Picture debugging a JSON response from a Laravel API endpoint. If the data is missing a field, you’d check the controller or the query – maybe you forgot to select that field or it’s named differently. Likewise, if the AI answer misses a piece of info, it might be because your prompt didn’t mention that piece or implied a different focus. By methodically adjusting and re-running (and sometimes reverting if an adjustment made things worse), you converge on a prompt that consistently yields good results. It’s literally prompt debugging. Just as you write tests for critical code paths, you might test your prompts with a few variations of input to ensure the AI handles them well.
**Keep track of what works.** In code, we version control our changes; for prompts, it’s helpful to keep notes or versions of prompts that worked versus those that didn’t. You might discover patterns – e.g., “When I phrase it like X, the answer is more accurate, but phrasing like Y causes confusion.” Over time, the lessons you learned become your personal prompting playbook.
Finally, remember that AI models can update or change (just like dependencies). A prompt that worked perfectly with one model (say GPT-3) might need tweaking for another (say GPT-4) because of differences in how they handle instructions. So if you upgrade your AI model, be ready to re-test your prompts. It’s an evolving process, but that’s part of the fun – you’re not just writing a prompt, you’re developing it.
#### Extending prompting with RAG and vector stores
Sometimes the information you need is outside the AI’s built-in knowledge. Imagine you have a Laravel project with extensive documentation or a private knowledge base. How do you get the AI to use that? Enter Retrieval-Augmented Generation (RAG) – a technique that combines prompting with information retrieval. In a RAG setup, your prompt isn’t flying solo; it’s backed by a mini search engine that supplies relevant data from external sources. The system will fetch, say, the portion of your Laravel docs about middleware, and feed it into the prompt so the model can generate a more informed answer.
In practical terms, RAG works like this: first, you query a knowledge store (documentation, wiki, database, etc.) for information related to your question, then you attach those snippets of information to the prompt you give the LLM. The LLM sees both your question and the retrieved text, allowing it to produce a response that’s grounded in that external data. This approach dramatically improves accuracy and relevance for domain-specific questions – it’s like giving the AI an open-book exam. Instead of hoping it remembers some Laravel detail from training, you hand it the exact page from the docs and ask it to answer using that.
To enable RAG in your applications, you’ll often use vector databases behind the scenes. These are specialized databases that store embeddings (vector representations) of text, which allows for efficient similarity search – a fancy way of saying “find me chunks of text related to this query.” When you ask a question, the system converts it to a vector, searches the vector DB for similar content (like the relevant Laravel guide about routing), and gets back those text chunks to include in the prompt. Vector databases play a key role in RAG systems: they enable fast, relevant context retrieval to feed to the LLM. Popular choices include Pinecone, Weaviate, or even open-source ones like Qdrant or FAISS, but the concept is what’s important: it’s your knowledge bank for the AI.
Consider a practical scenario: you’re building a ChatGPT-like assistant for your company’s internal Laravel codebase. You want it to answer questions about the code (“Where is the user authentication logic defined?”) or documentation (“What does our XyzService class do?”). By using RAG, your app can search your actual code or docs for the keywords, pull the relevant snippets (perhaps the XyzService class definition or docstring), and prepend them to the AI prompt: “According to our docs, XyzService is responsible for \[…\] . Given that, how would I extend it to add feature Y?”. The LLM, now armed with real data, can give a far more precise answer, even citing the provided context. This easily beats a vanilla prompt because the AI isn’t guessing or relying on possibly outdated training data – it’s using your current, source-of-truth information.
**One more benefit:** reduced hallucination. Hallucinations (when the AI confidently invents incorrect info) are a big headache, especially with technical info. By grounding the model with actual retrieved facts, you anchor its imagination. It’s as if you clipped its wings a bit – in a good way – keeping it factual. Of course, implementation matters: you’ll need to ensure your retrieved context is relevant (garbage in, garbage out). Tools like vector stores help by finding semantically relevant chunks of text rather than doing simple keyword matching.
In summary, RAG and vector databases let you augment the AI’s knowledge on the fly. For a developer, this is like having a self-updating Stack Overflow or documentation buddy integrated into your AI assistant. Instead of the assistant saying “I don’t know” or, worse, making something up about Laravel, it can pull up the actual docs and give you an answer with confidence. It’s a powerful technique that extends what your prompts can do – by plugging in the right data at the right time – ensuring your AI assistant stays as sharp and accurate as the documentation and data you feed it.
*This is part 1 of a series on how to write better prompts for AI assistants. [Click here to read part 2.](https://kirschbaumdevelopment.com/insights/anatomy-of-a-prompt-for-ai-assistants)*
### Anatomy of a prompt for AI assistants
#### Part 2
*Published on April 7, 2025*
---
*This is part 2 of a series on how to write better prompts for AI assistants. [Click here to read part 1.](https://kirschbaumdevelopment.com/insights/crafting-effective-prompts-for-ai-assistants)*
Crafting a great prompt is like taking an X-ray of your request. You need to see all the essential parts lined up correctly. A well-structured prompt typically includes several key components that work together to guide the AI towards a high-quality response. In this section, we’ll dissect a prompt into its core sections step by step, explain the purpose of each part, and explore how to optimize them. We’ll also cover how long each section should be, common pitfalls to avoid, why repeating crucial info can help, and some extra tricks to make your prompts even better. Let’s put a prompt under the X-ray and examine its “bones”!
#### Sections of a prompt
A robust prompt often contains a few main sections, each serving a specific purpose in steering the AI. Not every prompt will need all of these, but understanding them helps you assemble prompts more effectively. The common components are:
- **Role/context:** This section sets the scene or persona for the AI. You might define who the AI is or provide background info. For example: “You are a senior Python developer and database expert.” Establishing a role or context helps the model adopt the right tone and knowledge base for the task. It gives the AI a frame of reference for its response (e.g. an expert, a friendly advisor, a specific context or scenario).
- **Instruction (directive):** The instruction is the core task you want the AI to perform. It should be a clear, explicit command or question. For instance: “Write a function that calculates the average of a list of numbers.” This directive tells the model exactly what output or action is expected. A well-defined instruction is critical. Without it, the AI may respond irrelevantly or wander off-topic.
- **Input data/examples:** If the task involves some data or examples, include them in the prompt. This is the information the model needs to refer to in order to perform the task. For example, you might provide a piece of text to summarize, a JSON snippet to parse, or a few example QA pairs to demonstrate a format. Clearly delimit this input (e.g. with quotes, code blocks, or separators) so the AI knows it’s reference material and not part of the instruction. Including relevant data or examples helps ground the model’s response in facts or desired patterns.
- **Expected output/format:** A high-quality prompt often specifies what form the answer should take. This section describes the desired output format or style. For example: “Provide the answer as a JSON object.” or “Output a bulleted list of three points.” By telling the AI how to format its answer, you reduce ambiguity and get results that are easier to use. This acts as an output indicator guiding the model on how to present the answer.
- **Constraints/guidelines:** Constraints are additional rules or caveats for the response. They might include length limits, style guidelines, or things to avoid. For instance: “The explanation should be no more than two sentences and use simple language.” or “Do not mention any internal variable names in the output.” Constraints fine-tune the response, ensuring it meets specific requirements (like brevity, tone, or avoiding certain content). They keep the AI from going off-track or giving undesired output.
#### Dissecting a prompt:
To see these sections in action, let’s examine a sample prompt a developer might write:
```
You are an expert Python developer and data analyst.
Below is a list of numbers:
[2, 4, 6, 8, 10]
Task: Write a short Python function to calculate the average of these numbers.
Provide only the code and a one-sentence explanation of how it works.
The code should not use any external libraries.
```
This prompt contains all the key sections:
- Role/context: “You are an expert Python developer and database analyst.” Sets the expertise and context for the AI.
- Input data: The list \[2, 4, 6, 8, 10\] is provided as the data the function will use. It’s clearly separated and gives the model something concrete to work with.
- Instruction: “Write a short Python function to calculate the average of these numbers.” Tells the AI exactly what to do (the main task).
- Expected output/format: “Provide only the code and a one-sentence explanation of how it works.” Specifies the format: the answer should include the code solution and a brief explanation, nothing more.
- Constraints: “The code should not use any external libraries.” Adds a rule the solution must follow, in this case limiting which tools can be used.
By structuring the prompt this way, we’ve guided the AI with context, given it data to act on, explicitly stated the task, described the required output format, and set constraints. Each section plays a role in reducing uncertainty and focusing the model’s efforts on what we actually want.
#### Ideal length for each section
How much detail should you include in each part of the prompt? It’s important to find a balance: enough detail to be clear, but not so much that you drown the model in noise. There’s a tension between providing necessary context and keeping the prompt concise. Here are some guidelines on length for each section:
- Role/context: Usually 1-2 sentences are enough for setting the role or context. A short phrase can do the job (“You are a helpful customer support agent…”). If you need to include background info or a scenario, keep it to a brief paragraph at most, focusing only on details that will influence the answer. Avoid lengthy lore or backstory that isn’t directly relevant. Extraneous context can confuse the model or distract it.
- Instruction: Aim to state the task in one clear sentence or a single question if possible. In more complex cases, you might use a couple of sentences or break the task into sub-bullets, but brevity is generally better. Overly long or compound instructions can be hard to parse. If you find your instruction running on and on, consider that it might be doing too much at once. Try splitting the task into smaller steps (we’ll cover this in “Other tricks” below).
- Input data/examples: Include as much input data as needed for the task, but only what’s needed. If the model has to analyze or transform provided text/code, you may need to paste it in full (even if it’s long). Use delimiters (like triple backquotes ``` or a clearly labeled section) to separate this input from the rest of the prompt for clarity. If the input data is huge (thousands of words), consider whether a summary or a smaller excerpt would suffice, as very long inputs can exhaust the token limit or cause the model to lose focus. Essentially, give the model enough information to work with, but no more than necessary.
- Expected output/format: This usually can be expressed in a short phrase or sentence. You might even format it as a bullet list of criteria. For example: “Output format:\\n- JSON with keys name and age”. Being concise here is fine, as long as the requirement is unmistakably clear. If the output needs multiple specifications (e.g. style, length, format), it’s often better to list them as separate bullet points for readability.
- Constraints/guidelines: These should be brief and specific. It’s common to list constraints as a few bullets or sentences at the end of the prompt. Each constraint might be just a clause (e.g. “Max 100 words,” “Explain in layman’s terms,” “No references to the prompt itself”). Too many constraints can over-complicate things or even conflict with each other, so don’t include needless rules. Stick to the critical ones that ensure the response meets your goals. If you have more than 3-4 constraints, ask yourself if they’re all truly necessary.
**In summary, be as detailed as needed, but as succinct as possible for each part.** The prompt should give the AI just enough information to do the job and no excess fat. If any section of your prompt can be shorter without loss of clarity, shorten it. Conversely, if making something a bit longer would prevent ambiguity, add those few extra words. Finding the sweet spot comes with practice and sometimes a bit of trial and error.
#### Common “gotchas” and pitfalls
Even with a good structure, prompts can fail due to some common mistakes. Here are a few prompt “gotchas” to watch out for, and how to fix them:
- **Vague phrasing:** If your prompt is vague, the AI might produce a broad or irrelevant answer. For example, asking “Tell me about technology.” is so open-ended that you could get practically anything back. Vague or generic prompts often lead to generic or off-target responses. Fix: Be specific about what you want. Identify the particular aspect or angle you’re interested in. Instead of “tell me about technology,” you might ask, “Explain three ways quantum computing could impact cybersecurity.” The more precise your wording, the more focused the answer will be.
- **Over-explaining or prompt bloat:** On the flip side, stuffing the prompt with unnecessary detail, long-winded context, or irrelevant information can confuse the model. If you bury a simple question inside a paragraph of fluff, the model might miss the point or latch onto the wrong detail. Overly complex, convoluted prompts can lead to convoluted responses. Fix: Trim the fat. Remove details that don’t directly affect the task. Keep context short and pertinent. You can certainly provide context, but don’t turn your prompt into a novel unless you actually need the model to consider all that information. Aim for clarity and simplicity over sheer length.
- **Ambiguous instructions:** Ambiguity is the enemy of helpful AI output. For instance, “Draw the bank card” is unclear and could mean a financial diagram or literally sketching a credit card. Similarly, “Write about Python” doesn’t specify if you mean the programming language or the snake. Ambiguity often yields answers that don’t address your actual need. Fix: Identify ambiguous terms or double meanings and clarify them. If a word could be interpreted in multiple ways, rephrase it or add detail. In our example, you’d do better to say “Generate an image of a credit card” vs. “draw,” or “Write about the Python programming language’s history.” Provide enough detail so there’s only one reasonable interpretation of the request.
- **Unrealistic ask:** Sometimes prompts fail because they ask for the impossible or the model misinterprets feasibility. For example, “Predict the stock market next week with 100% accuracy.” The model will either produce a poor guess or refuse. While not exactly a structural issue, it’s a pitfall in what you’re asking. Fix: Keep requests within the model’s capabilities and knowledge. If you need prediction or external data, reframe the task (e.g., “List factors that could influence stock prices” instead). Also, avoid contradictory instructions (e.g., “Give a detailed answer in one word” sets an impossible task). Ensure your constraints and asks align logically.
- **Ignoring format guidelines:** A subtle pitfall is when the prompt does specify a format, but the instructions are buried or not emphasized, so the model might ignore them. If you said in the middle of a long prompt, “answer must be JSON,” the model might miss it, giving a narrative answer instead. Fix: Make format instructions stand out (e.g., as the final line or in a list) and consider reiterating them (more on repetition next). Clearly separating format requirements (using formatting or keywords like “Output format:”) helps the model register that constraint. Many providers support JSON structured output, and that can be leveraged in projects that can benefit from it.
By being mindful of these pitfalls, you can debug prompts that aren’t working well. If you get a weird or wrong output, reread your prompt and check: Was I vague? Did I include extraneous info? Is my request clear and achievable? Oftentimes, a quick rewrite addressing these issues will fix the model’s response in the next try.
#### Advantages of repetition and reinforcement
When it comes to crucial details in your prompt, it can pay off to be a bit repetitive (in a smart way!). Large language models can be influenced by recency bias, meaning they pay more attention to the last things said in the prompt. This implies that restating key instructions or important information at the end of your prompt can reinforce those points and make the model more likely to follow them.
Why repeat? Reinforcement helps ensure the model doesn’t “forget” important constraints or context, especially in longer prompts. For example, if your prompt includes a long background and then your question, the model might lose track of a detail mentioned only once in the beginning. By repeating or summarizing that critical detail in the instruction or at the end (“Remember, use the data above in your analysis”), you remind the model of what matters most. Think of it as highlighting the main points for the AI.
What to repeat: You should repeat the key requirements or nuances that are absolutely essential to get right. This could be the desired output format, the main topic, or a do/do-not rule. For instance, if it’s vital that the answer is in bullet points, you might state at the top and bottom, “The answer should be a bulleted list.” If the tone must be professional, you might weave that into the role and also say “(in a professional tone)” again in the prompt.
**Reinforcement vs. redundancy:** Be careful to repeat strategically, not aimlessly. You don’t want to confuse the model with contradictions or lots of noise. It’s usually best to repeat by paraphrasing or summarizing the crucial instruction. For example: “Explain the code in one sentence.” at the end echoes the earlier constraint of brevity. This consistency makes the instruction hard to miss. On the other hand, avoid repeatedly emphasizing something trivial or repeating undesired words. (If you keep saying “Don’t mention X” several times, **you’re actually fixating the model on “X”** which can backfire by making it more likely to mention it!). So, repeat the positive instructions of what you do want.
**Recency matters:** As noted, models tend to give weight to the last part of the prompt. A practical tip is to end your prompt on a strong note: either end with the main question/instruction itself, or a quick recap of the most important constraint. For example: “Provide the three insights as JSON. Remember: output only valid JSON.” The final reminder (“output only valid JSON”) sits at the end, leveraging recency to nudge the model in the right direction.
In summary, a bit of deliberate echoing in your prompt can significantly improve reliability. If there’s something you absolutely need in the answer, don’t shy away from reinforcing it. The model is more likely to comply when it’s seen that requirement multiple times in clear terms. Just make sure those repetitions are clear, consistent, and focused on your goal.
#### Other tricks and optimizations
Once you have the basic prompt structure down, you can employ various techniques to make your prompt even more effective. Here are some extra tricks and optimizations, especially handy for developers and advanced prompt crafters:
- **Use clear formatting and delimiters:** Structure your prompt so that each part is unambiguous. You can use separators like --- or triple backticks to isolate different sections (e.g. one for context/data, one for instructions). For instance, if you include a block of text or code for the AI to act on, wrap it in triple quotes or a code block. This way, the model knows exactly what text is the data or example and what is the actual question. Clear formatting prevents the AI from mixing up instructions vs. input. It also improves readability for you and the model. Many prompt guides recommend delimiters because they reduce confusion and even help avoid inadvertent prompt injection .
- **Ask for structured output:** If you need the answer in a particular structure (JSON, XML, a table, bullet points, etc.), explicitly ask for it. You can even provide a template or an example of the desired format. For example: “Answer in JSON with keys status and message.” or “Respond in a markdown table with columns X, Y, Z.” When you specify the format, the model will usually try to match it . This is extremely useful for developers who plan to parse the output. It saves time cleaning up the answer. If necessary, demonstrate the format (e.g., give a dummy JSON or a partial example) to eliminate any guesswork. Many providers support structured output via specialized instructions sent via JSON schemas. Leverage them when it makes sense for your project.
- **Break complex tasks into steps:** Don’t hesitate to guide the AI through a multi-step reasoning process. LLMs often do better when they’re instructed to tackle a problem step-by-step. You can prompt this in a couple of ways. One way is to explicitly enumerate subtasks: “1. First, summarize the user input. 2. Then, check for any contradictions. 3. Finally, output a conclusion.” By listing the steps in the prompt, you help the model organize its approach. Another way is to ask the model to “think aloud” or reason before the final answer (e.g., “Explain your reasoning, then give the answer”). This is related to chain-of-thought prompting, where the model’s intermediate reasoning leads to a more accurate final result. In any case, breaking down the task can prevent the model from getting overwhelmed or making leaps of logic.
- **Control verbosity with explicit cues:** You can influence how verbose or concise the model is by explicitly stating your preference. If you want a brief answer, say so: “(Answer in one sentence.)” or “Keep the explanation under 50 words.” On the other hand, if you want a detailed answer, encourage depth: “Provide a step-by-step analysis… elaborate on each point.” You can also prime the output by phrasing a cue at the end of the prompt. For example, ending the prompt with “Answer in a single sentence:” will cue the model to be concise (it sees the colon and likely fills in one sentence). Conversely, “Explain in detail:” suggests a longer answer. Being direct about length and detail can greatly help the model hit the target response length. Remember, the model doesn’t inherently know if you want a summary or an essay unless you tell it.
- **Iterative prompt refinement:** Treat prompt-writing as an iterative process. Rarely will a complex task be perfect on the first try. Developers often try a prompt, see how the AI responds, and then tweak the prompt to fix any issues. For example, if the output wasn’t in the right format, you can add a line to your prompt explicitly instructing that format. If the answer was off-topic, you may need to add a clarifying detail or constraint. Each iteration is a chance to sharpen the prompt. A good workflow is: test the prompt with the AI, examine the response, adjust the prompt, and repeat. Over a few iterations, you’ll converge on a prompt that consistently yields high-quality results. This practice is essentially using the AI as your collaborator to zero in on the best phrasing. Don’t be afraid to experiment.
- **Leverage bullet points and lists:** When asking for multiple items or providing multiple criteria, use bullet points or numbering in your prompt. If you ask a question in paragraph form with several questions embedded, the model might skip one. But if you format it as “1. Do X, 2. Do Y, 3. Do Z,” the model is more likely to address each part in order. Likewise, if you want an answer in list form, literally say “Give the answer as a list:” or provide a template like “- First insight\\n- Second insight\\n- …”. Models respond well to list structures and will often mirror them in the output. This trick not only improves completeness but also readability.
- **Use delimiters or tags for dynamic content:** If your prompt includes changing content (like user input in a larger system prompt), consider tagging it with identifiers. For example: … around a user query in a system prompt. While the AI doesn’t literally require XML tags, clearly marking sections can help if your prompt is programmatically constructed. It’s an organizational tool that can prevent the model from, say, confusing your system notes with the user’s query. Some developers use comments or labels like “Context:” and “Question:” to similar effect.
- **Test edge cases:** As a final optimization, think of possible misunderstandings and test your prompt against them. If your prompt could be interpreted in two ways, try phrasing it both ways to see how the model reacts. If the task is critical, test slightly varied prompts or add explicit clarifications to handle those edge cases. It’s easier to adjust the prompt before deployment than to get a surprise later. This goes hand-in-hand with iterative refinement as you’re essentially stress-testing your prompt to make sure it’s foolproof.
Good techniques help transform a decent prompt into a highly effective one. Keep in mind that not every trick is necessary for every prompt. Use the ones that make sense for your situation. Over time, you’ll develop an intuition for which prompt optimizations yield the best results for the task at hand.
### Supercharge Laravel development with AI
#### using Cursor and Gemini
*Published on May 2, 2025*
---
The world of software development is constantly evolving, and the recent advancements in artificial intelligence are undeniably changing the game. As a developer, I've been exploring how these powerful tools can enhance my workflow, particularly for building Laravel applications. It's a landscape that shifts rapidly, with new models and tools emerging constantly.
This article shares my journey navigating this landscape. I'll start by comparing some of the leading AI models I've evaluated for coding tasks. Then, I'll dive into Cursor, an AI-first code editor that has become central to my process. Most importantly, I'll detail the specific, structured workflow I've developed which combines manual scaffolding, meticulous AI planning using tools like Repomix and Google AI Studio, and agent-driven execution in Cursor. Finally, I'll share some crucial reflections on balancing AI assistance with developer ownership, the importance of review, and necessary considerations like security.
#### Choosing your AI co-pilot: Gemini vs. Claude vs. GPT-4o for coding
When it comes to AI coding assistants, things move fast. Right now, **my personal go-to is Gemini 2.5 Pro**. Why? For me, it currently feels like the most advanced model specifically for coding tasks. It's impressively fast, and the fact that it's often cheaper, or even free during its preview, is a major plus. I also find its approach valuable; it seems to 'think' through the problem before providing an answer, which often leads to better, more robust code compared to others.
So, how does it stack up against other strong contenders like **Claude 3.7 Sonnet** and **GPT-4o**?
**Gemini 2.5 Pro** is getting a lot of buzz, often called the "best AI for coding" right now. It performs very well on technical benchmarks and, in practical tests I've seen or run, it tackles complex coding challenges effectively. Think generating working code for things like flight simulators or tricky algorithms, often in one shot. Its massive 1 million token context window (with 2 million potentially available) is a game-changer, allowing it to understand huge codebases. While it's fantastic for complex system building and even fixing code generated by other models, it's not perfect. I've encountered occasional bugs, and some users note its general reasoning outside of pure coding isn't always top-tier, though its vision capabilities are strong. The free tier also comes with rate limits.
**Claude 3.7 Sonnet** is another powerhouse. Many developers rave about its ability to generate remarkably clean, almost bug-free code right out of the gate, often outshining GPT-4 in reliability. Its real strength, in my opinion, is its clarity – it explains code well and its 'Thinking Mode' (though often requiring a paid subscription) is fantastic for step-by-step debugging. However, its context window is smaller (200k tokens), which can be a limitation for very large projects, and it can be pricier than Gemini. While generally reliable, I've seen reports where it struggled with the most complex generation tasks that Gemini handled.
And what about **GPT-4o**? It brings strong multimodal capabilities to the table and is generally cheaper than Claude. Its large context window is also a benefit for understanding code context. However, based on recent comparisons and user feedback, it seems to be lagging behind both Gemini 2.5 Pro and Claude 3.7 Sonnet specifically for complex coding tasks. Many find it produces buggier code or struggles to follow intricate instructions compared to the other two.
**In short**: For my current needs, **Gemini 2.5 Pro** offers the best blend of cutting-edge coding performance, large context understanding, and cost-effectiveness. **Claude 3.7** is an excellent, highly reliable alternative, especially if you value near bug-free code and clear debugging assistance. **GPT-4o**, while versatile, doesn't quite seem to match the specialized coding strengths of the other two right now. Naturally, this landscape is constantly evolving, so what's best today might change tomorrow!
While choosing the right base model is important, the development environment integrating it also plays a huge role. That led me to explore tools specifically designed for an AI-centric workflow, like Cursor.
#### Deep dive: Cursor IDE - an AI-first approach
Another interesting tool I've explored is **Cursor**, an AI-powered code editor built on top of VS Code. If you're already comfortable with VS Code, the transition feels pretty seamless because the layout and shortcuts are largely the same. What sets Cursor apart is how deeply AI is woven into the core experience, rather than feeling like an add-on extension.
Here are some of the features I've found most noteworthy:
- **AI completions (tab key)**: This is probably Cursor's standout feature for many. Its tab completion goes way beyond standard suggestions. It genuinely feels like it anticipates your next move, suggesting entire lines or blocks of code, finding bugs, and proposing fixes, all incredibly quickly. It's a subscriber feature, and while generally brilliant, sometimes the suggestions disappear fast or aren't quite right.
- **Chatting with your code**: Cursor offers several ways to "talk" to your code using AI models. You can make quick inline edits (Cmd/Ctrl+K), have longer conversations in a sidebar (Cmd/Ctrl+L) for bigger refactors or generating new files, or use the 'Composer' for complex changes across multiple files. It's quite versatile for manipulating and generating code through natural language prompts.
- **Agent mode**: This is where Cursor tries to handle tasks more autonomously, like figuring out which files to create or modify for a feature request (e.g., building a new page). It's powerful when it works, but requires very clear instructions, especially in large projects, to avoid unintended changes. It can even run terminal commands (with confirmation).
- **Guiding the AI with Project Rules**: This is crucial for teamwork and maintaining standards. Instead of the older (and now deprecated) .cursorrules file, Cursor now uses Project Rules stored in a .cursor/rules directory within your project. These version-controlled files let you provide specific instructions to the AI. Think enforcing architectural patterns, coding conventions, or specifying tech stack details. It's essentially a way to encode your team's knowledge and preferences, ensuring the AI generates code that's consistent and high-quality. You can define rules that always apply, are triggered automatically based on file patterns, or are only used when the AI deems them relevant. There are also global User Rules you can set for personal preferences like response style.
- **Understanding your codebase**: Cursor works hard to grasp the context of your project. It uses codebase indexing (which you can disable) and allows you to easily reference specific files, documentation, or even perform web searches using the '@' symbol in chat to give the AI better context.
##### Overall experience:
When Cursor shines, it really shines, significantly speeding up development, particularly with its tab completion and refactoring tools. Using Project Rules helps maintain code quality and consistency, which is great for teams.
However, it's not without its quirks. The UI can feel a bit cluttered with all the AI elements. Like any AI tool, suggestions can sometimes be off-base or even counterproductive. Agent mode needs careful handling, and there's a learning curve to mastering all its features.
Compared to simpler extensions like GitHub Copilot, Cursor offers a much more integrated and feature-rich AI environment. It typically has a free tier with limits and a Pro plan (around $20/month) for more extensive use.
For me, Cursor represents a fascinating step towards AI-native development environments. While it requires some learning and adaptation, its potential for boosting productivity, especially with features like intelligent tab completion and configurable Project Rules, makes it a tool worth watching and experimenting with.
Seeing Cursor's potential, especially for complex tasks using its Agent mode, I realized I needed a structured way to harness its power effectively. And from the Cursor workshop from John Lindquist (https://egghead.io/workshop/cursor). This led me to develop a specific workflow, particularly tailored for my Laravel projects.
#### My structured workflow for AI-assisted Laravel development
For tackling substantial features or tasks in my Laravel projects with Cursor, I've learned a structured approach works best. It generally follows these steps: **Scaffold -> Plan -> Execute -> Monitor**.
##### Step 1: Scaffold first (using Artisan & Composer)
I start by creating the basic building blocks using the standard command-line tools. Relying on the AI to initialize things from scratch can sometimes lead to outdated templates or missed configurations. So, whether it's a new project (`laravel new my-app` or `composer create-project laravel/laravel my-app`) or adding new dependencies to an existing one, I lay the foundation manually first. This ensures I'm using the latest official structures and dependencies.
##### Step 2: Plan (gather context & instruct the AI)
This is the most crucial step. Before asking Cursor's agent to perform complex tasks, I need to give it a detailed plan and all the necessary context.
- **Gather full context**: I use a tool like FileForge (`ffg -y --template plan`) or RepoMix (`npx repomix@latest > context.txt`) to create a comprehensive snapshot of my project. This usually generates an XML-like file containing the directory structure and the content of relevant files (respecting `.gitignore`). I make sure this includes key code files, configuration files (like `.env.example`, `config/*.php`), and importantly, any custom **Cursor Project Rules** I've defined in the `.cursor/rules` directory. Providing everything upfront prevents the AI from making assumptions or generating incorrect code (like a wrongly formatted config entry). For huge projects, I use exclude/include flags in these tools to keep the context manageable.
- **Instruct a capable AI**: I copy this entire context package and paste it into a powerful AI model that has web search capabilities (my preference is **Gemini 2.5 Pro** via AI Studio, but others could work). Along with the context, I provide clear instructions, typically including:
- **General workflow**: How I want it to handle Git (e.g., feature branches, commit message format), testing preferences (e.g., "Generate Pest tests for new features"), and package manager usage (`composer install` vs `update`).
- **Laravel specifics**: Explicit instructions like "Always use `herd php artisan ...` for local commands," "Generate code following Laravel best practices," "Ensure new routes are added to `routes/api.php`," or "Use dependency injection in controllers."
- **The task**: A clear description of what needs to be done, asking for a step-by-step plan. For example: "Generate a detailed, step-by-step guide including code snippets and exact `php artisan` commands to add a new feature that allows users to upload profile pictures. This should include creating the necessary migration, model updates, controller logic, API routes, validation rules, and basic Pest tests."
##### Step 3: Execute (run the plan in Cursor)
Once the external AI provides a detailed, step-by-step plan with specific code and commands based on my prompt, I copy that entire plan. I then start a new Cursor agent session (Cmd+N, then Cmd+I) and paste the plan into the agent input, making sure auto-run is enabled. Cursor then attempts to execute the plan step-by-step.
##### Step 4: Monitor & nudge (guide the agent)
I watch Cursor execute the plan. It often works smoothly, but sometimes it needs guidance. If it stalls, makes a mistake (like using a wrong command or putting code in the wrong file), or gets stuck:
- **Interrupt**: I use Cmd+Shift+Backspace.
- **Correct**: I provide specific feedback. I might paste the error message and say, "You need to run `herd php artisan migrate` first," or "That method should be in the `UserProfileController`, not the `UserController`," or "You forgot to import the `Storage` facade." Simple prompts like "Fix, please" can sometimes work for minor errors, but targeted instructions are usually better.
- **Restart**: If things go significantly off track, I might use Cursor's "Reject All" (though I often prefer resetting changes with Git) and restart the agent session with a slightly revised plan based on what went wrong.
This Scaffold -> Plan -> Execute -> Monitor process might seem elaborate, but I find it's the most reliable way to leverage Cursor's agent capabilities for complex development tasks in Laravel, minimizing errors and ensuring the AI follows project standards and my specific instructions.
A key part of making this workflow reliable, especially the 'Execute' step with the Cursor agent, involves guiding the AI effectively. This isn't just about the initial plan; it's also about setting persistent guidelines within the editor itself. This is where Cursor's Project Rules become essential.
#### Taming the AI: guiding Cursor with Project Rules
Simply having the AI access your code isn't always enough. To get truly consistent and high-quality results, especially when working in a team or on complex projects, you need to guide the AI. This is where Cursor's **Project Rules** come in, which I find essential.
First off, it's important to use the newer **Project Rules** system, which stores rules as individual files in a `.cursor/rules` directory within your project. The older, single `.cursorrules` file in the root directory is still supported for now, but it's deprecated, and the new system offers more flexibility.
I recommend starting with a general rule that explains the overall architecture, key business logic, and core technologies of your project. This gives the AI foundational knowledge. Here’s an example of an architecture rule I use for one of my Laravel/React/Inertia projects:
```
---
description:
globs:
alwaysApply: true
---
# Architecture Documentation: tts.test SaaS Platform
## 1. High-Level Overview
### System Purpose and Functionalities
The system is a Software as a Service (SaaS) platform designed primarily for **Spanish Text-to-Speech (TTS) synthesis**. Based on the codebase structure (`Project`, `ProjectAudioGeneration` models, TTS service integration), its core functionalities include:
- **User Authentication & Management:** User registration, login, password management, profile updates, and email verification.
- **Team Management:** Users belong to teams, can own teams, and have a concept of a "current team" for context switching.
- **Project Management:** Users can create projects within their current team to organize TTS tasks.
- **Text-to-Speech (TTS) Generation:** Users can input text (via a rich text editor - Tiptap), select a voice, and initiate an asynchronous job to generate an audio file using an external TTS service (Google Cloud TTS).
- **Audio Management:** Generated audio files are stored (using Spatie MediaLibrary) and associated with their respective generation tasks and projects. Users can preview/play generated audio within the application.
- **Settings:** Users can manage profile details, passwords, and interface appearance (light/dark/system themes).
### Architectural Style
The system follows a **monolithic, client-server architecture**.
- The backend is a traditional **Laravel (PHP)** application adhering largely to the **Model-View-Controller (MVC)** pattern, extended with **Action classes** to encapsulate specific business logic.
- The frontend is a **React (TypeScript) Single Page Application (SPA)**.
- **Inertia.js** acts as the bridge between the Laravel backend and the React frontend, allowing the backend controllers to directly render React page components and pass data without requiring a separate REST/GraphQL API.
- **Asynchronous processing** is used for time-consuming tasks like TTS generation via Laravel Queues and Jobs.
### Core Technologies, Languages, and Libraries
- **Backend:**
- Language: PHP 8.4+
- Framework: Laravel 12.x
- ORM: Eloquent
- Queue System: Laravel Queues (Configured for Database driver by default)
- Database: PostgreSQL (Migrations set up, default connection often SQLite for local dev via `.env.example`)
- Key Libraries:
- `inertiajs/inertia-laravel`: Bridge to Inertia frontend.
- `tightenco/ziggy`: Share Laravel routes with JavaScript.
- `spatie/laravel-data`: Data Transfer Objects (DTOs).
- `spatie/laravel-medialibrary`: File/Media management.
- `google/cloud-text-to-speech`: Google Cloud TTS client library.
- **Frontend:**
- Language: TypeScript
- Framework/Library: React 19.x
- UI Components: shadcn/ui (built on Radix UI and Tailwind CSS)
- State Management: Primarily via Inertia props; component-level state managed by React hooks.
- Routing: Inertia.js (client-side), Laravel (server-side definition).
- Build Tool: Vite
- Styling: Tailwind CSS v4
- Rich Text Editor: Tiptap
- **External Services:**
- Google Cloud Text-to-Speech API
## 2. Component Interactions
### Major Components
- **Frontend (React/Inertia - `resources/js`)**:
- `pages/`: Contains React components representing individual application pages (e.g., `dashboard.tsx`, `Project.tsx`, `settings/profile.tsx`). Rendered by Inertia based on backend responses.
- `layouts/`: Structural components defining page layouts (e.g., `AppSidebarLayout`, `AuthSimpleLayout`, `SettingsLayout`).
- `components/`: Reusable UI elements (`ui/` - shadcn/ui primitives, `app-logo.tsx`, `TiptapEditor.tsx`, `AudioPlayer.tsx`, etc.).
- `hooks/`: Custom React hooks (e.g., `useAppearance`, `useMobile`).
- `lib/`: Utility functions (e.g., `utils.ts`).
- **Backend (Laravel - `app/`)**:
- `Http/Controllers/`: Handle incoming HTTP requests, interact with Actions/Models, and return Inertia responses (e.g., `DashboardController`, `ProjectController`, `Auth/RegisteredUserController`, `Settings/ProfileController`).
- `Http/Requests/`: Define validation rules and authorization logic for specific requests (e.g., `StoreProjectRequest`, `StoreProjectAudioGenerationRequest`). Often create DTOs.
- `Http/Middleware/`: Process requests before they reach controllers (e.g., `HandleInertiaRequests` shares data, `HandleAppearance` manages theme cookies).
- `Http/Resources/`: Transform Eloquent models into structured data for the frontend (e.g., `ProjectResource`, `UserResource`, `ProjectAudioGenerationResource`).
- `Actions/`: Encapsulate specific business logic operations (e.g., `CreateProjectAction`, `StartAudioGenerationAction`, `GetDashboardData`). Called by Controllers.
- `Models/`: Eloquent models representing database tables and relationships (`User`, `Team`, `Project`, `Voice`, `ProjectAudioGeneration`). Interact with the database.
- `Jobs/`: Define asynchronous tasks processed by the queue (`GenerateSpeechJob`).
- `Services/`: Wrapper classes for external services (`TextToSpeechService`).
- `Providers/`: Service providers for bootstrapping services (`TextToSpeechServiceProvider`).
- `Policies/`: Define authorization rules for model actions (`ProjectPolicy`, `ProjectAudioGenerationPolicy`).
- `Enums/`: Define enumerated types (`AudioGenerationStatus`).
- `Exceptions/`: Custom exception classes (`TextToSpeechGenerationException`).
- **Database (`database/`)**: Stores application state (users, teams, projects, voices, audio generations, media files, jobs, cache, sessions). Accessed via Eloquent Models. Migrations define the schema.
- **Queue System (`config/queue.php`, `app/Jobs/`)**: Manages background tasks. `GenerateSpeechJob` is pushed onto the queue and processed by a worker.
- **File Storage (`config/filesystems.php`, `storage/`)**: Stores generated audio files (via Spatie MediaLibrary, configured for local or cloud disks like `teams_cloud`).
- **External TTS Service (Google Cloud)**: Integrated via `TextToSpeechService` for audio synthesis.
### Interaction Patterns
- **Client-Server (Inertia)**: User interacts with the React frontend. Actions trigger Inertia visits/requests to the Laravel backend. Laravel Controllers handle these, often using Actions, Models, and Requests, then return an Inertia Response with a page component and data (transformed by Resources).
- **Controller-Action**: Controllers delegate complex business logic to dedicated Action classes (e.g., `ProjectController` uses `CreateProjectAction`).
- **Action-Model/DB**: Actions interact with Eloquent Models to read/write data from the database.
- **Request-Validation**: Form Requests automatically validate incoming data before controller/action logic executes.
- **Job Dispatch**: Controllers or Actions dispatch Jobs (e.g., `StartAudioGenerationAction` dispatches `GenerateSpeechJob`) to the queue for asynchronous processing.
- **Job-Service**: Jobs utilize Services to interact with external APIs (e.g., `GenerateSpeechJob` uses `TextToSpeechService`).
- **Service-External API**: Services make calls to external APIs (e.g., `TextToSpeechService` calls Google Cloud TTS).
- **Model-Media Library**: Models implementing `HasMedia` (e.g., `ProjectAudioGeneration`) interact with Spatie MediaLibrary to manage associated files.
- **Middleware Pipeline**: Requests pass through middleware for authentication, Inertia setup, etc.
### Mermaid Component Diagram
```mermaid
graph LR
subgraph "User Browser"
Frontend["React SPA (Inertia.js)"]
end
subgraph "Laravel Backend"
Middleware["Middleware Pipeline"]
Controllers["Controllers"]
Actions["Action Classes"]
FormRequests["Form Requests"]
Models["Eloquent Models"]
QueueSystem["Queue"]
Jobs["Jobs (e.g., GenerateSpeechJob)"]
Services["Services (e.g., TextToSpeechService)"]
MediaLib["Media Library (Spatie)"]
Policies["Policies"]
Resources["API Resources"]
Middleware -- Request --> Controllers
Controllers -- Uses --> Actions
Controllers -- Uses --> FormRequests
Controllers -- Returns Inertia Response --> Frontend
Actions -- Uses --> Models
Actions -- Dispatches --> QueueSystem
Jobs -- Processed by --> QueueWorker
Jobs -- Uses --> Services
Jobs -- Uses --> Models
Models -- Interacts with --> Database[(Database)]
Models -- Uses --> MediaLib
MediaLib -- Writes/Reads --> FileStorage[(File Storage)]
Services -- Calls --> ExternalTTS["External TTS API (Google)"]
Policies -- Authorizes --> Controllers
FormRequests -- Validates --> Controllers
Resources -- Formats Data --> Controllers
end
subgraph "Infrastructure"
QueueWorker["Queue Worker"]
Database[(Database)]
FileStorage[(File Storage)]
end
subgraph "External Services"
ExternalTTS
end
Frontend -- HTTP Request --> Middleware
```
## 3. Data Flow Diagrams
### Audio Generation
```mermaid
graph LR
subgraph "User Interaction - React Project Page"
A[User Edits Text] --> B(TiptapEditor);
C[User Selects Voice] --> D(Voice Select Dropdown);
E[User Clicks Generate] -- Form Submit (Text JSON, Voice ID) --> F["POST /projects/ID/generations"];
end
subgraph "Backend Processing"
F -- Request --> G["StoreProjectAudioGenerationRequest"];
G -- Validates Text/Voice, Checks for In-Progress, Authorizes --> G;
G -- Creates DTO --> H[StartAudioGenerationData DTO];
F -- Passes DTO --> I["ProjectAudioGenerationController store"];
I -- Calls Action --> J["StartAudioGenerationAction handle"];
J -- Creates Generation Record (Status: PENDING) --> K[(Database - project_audio_generations)];
J -- Dispatches Job --> L{Queue::dispatch};
I -- Redirects Back --> M["React Project Page (w/ Flash Message)"];
end
subgraph "Async Job Processing"
N[Queue Worker] -- Picks Up Job --> O["GenerateSpeechJob handle"];
O -- Finds Generation & Voice --> P[(Database - project_audio_generations, voices)];
O -- Checks Voice Active --> O;
O -- Gets Plain Text --> O;
O -- Calls Service --> Q["TextToSpeechService synthesizeAndSave"];
Q -- Calls External API --> R[Google Cloud TTS API];
R -- Returns Audio Content --> Q;
Q -- Saves Audio to Disk --> S[(File Storage - teams_cloud/local)];
O -- Adds Media Record --> T[Spatie MediaLibrary];
T -- Creates Media Entry --> U[(Database - media)];
O -- Updates Generation Status (COMPLETED/FAILED) --> K;
end
style K fill:#f9f,stroke:#333,stroke-width:2px
style P fill:#f9f,stroke:#333,stroke-width:2px
style U fill:#f9f,stroke:#333,stroke-width:2px
style S fill:#ccf,stroke:#333,stroke-width:2px
```
## 4. Design Decisions and Rationale
- **Laravel Framework:** Chosen likely for its rapid development capabilities, extensive ecosystem (authentication, ORM, queues, testing), and strong community support. Its MVC structure provides a standard organization.
- **Inertia.js with React:** Selected to build a modern SPA-like frontend without the complexity of managing a separate API backend. Allows Laravel controllers to directly serve React components, simplifying data flow.
- **TypeScript:** Used in the frontend for enhanced type safety, improved developer experience, and better code maintainability compared to plain JavaScript, especially in larger applications.
- **Shadcn/UI & Tailwind CSS:** Leveraged for the frontend UI. Provides unstyled, accessible component primitives built with Radix UI, styled using Tailwind CSS utility classes. This offers high customizability and consistency while speeding up UI development. Rationale: modern, utility-first CSS, component reuse.
- **Action Classes (`app/Actions`)**: Used to separate business logic from controllers. This promotes cleaner controllers, improves code organization, and makes the core logic more testable and reusable. Rationale: Single Responsibility Principle, maintainability.
- **Data Transfer Objects (DTOs - `spatie/laravel-data`)**: Used in `StartAudioGenerationData` and `ProjectData` (within `StoreProjectRequest`). Ensures structured, type-safe data transfer between layers (Request -> Action). Rationale: Improves code clarity and reduces errors from passing unstructured arrays.
- **Eloquent ORM:** Laravel's default ORM is used for database interaction. Rationale: Convention, ease of use, integrates well with Laravel features.
- **ULIDs for Primary Keys:** Modern choice over traditional auto-incrementing integers. ULIDs are sortable, unique across tables/systems, and less predictable. Rationale: Scalability, uniqueness.
- **Queues & Jobs (`app/Jobs`)**: TTS generation is offloaded to a background job (`GenerateSpeechJob`). Rationale: Prevents blocking HTTP requests for long-running tasks, improves user experience, allows for retries and scaling of workers.
- **Service Layer (`app/Services`)**: A dedicated `TextToSpeechService` encapsulates interaction with the Google Cloud TTS API. Rationale: Decouples external service interaction, makes it easier to test or swap implementations.
- **Service Provider (`app/Providers`)**: `TextToSpeechServiceProvider` handles the instantiation and dependency injection of the `TextToSpeechClient`. Rationale: Centralized service configuration, follows Laravel's IoC principles.
- **Spatie MediaLibrary (`spatie/laravel-medialibrary`)**: Used for handling the storage and association of generated audio files (`ProjectAudioGeneration` model). Rationale: Provides a robust and conventional way to manage file uploads and associations in Laravel.
- **API Resources (`app/Http/Resources`)**: Used to transform Eloquent models into consistent JSON structures for the Inertia frontend. Rationale: Controls data exposure, ensures consistent data format, decouples frontend from raw model structure.
- **Policies (`app/Policies`)**: Used for authorization logic (e.g., ensuring a user can only view/modify projects or cancel generations belonging to their team). Rationale: Centralizes authorization rules, keeps controllers cleaner.
- **Pest Testing Framework:** Used for writing tests. Rationale: Modern PHP testing framework with a focus on readability and developer experience.
## 5. System Constraints and Limitations
- **External Dependency (Google Cloud TTS):**
- Requires valid Google Cloud credentials (`GOOGLE_APPLICATION_CREDENTIALS`) and a configured Project ID (`GOOGLE_CLOUD_PROJECT`).
- Subject to Google Cloud pricing, usage limits, and potential API changes.
- Network connectivity to Google Cloud APIs is essential for TTS functionality.
- **Asynchronous Processing:**
- Requires a running queue worker (`php artisan queue:work` or similar) to process `GenerateSpeechJob`. Without it, audio generation will remain pending.
- Queue configuration (`config/queue.php`) determines reliability and throughput (e.g., database driver might not be ideal for high load vs. Redis/SQS).
- **File Storage:**
- Requires correctly configured filesystem disks (`config/filesystems.php`), especially the `teams_cloud` disk if using cloud storage (like S3 or DO Spaces). Credentials and bucket names must be set in `.env`.
- Storage costs apply if using cloud storage.
- Local storage (`public` disk linked via `storage:link`) might not be suitable for production scaling or multi-server deployments.
- **Inertia SSR:**
- The configuration (`config/inertia.php`) enables Server-Side Rendering. This requires Node.js on the server and a running SSR process (`php artisan inertia:start-ssr`) for optimal initial page loads. If SSR is not running or fails, Inertia falls back to client-side rendering.
- **Environment Configuration:** The application relies heavily on environment variables defined in `.env` (based on `.env.example`) for database connections, API keys, URLs, etc. Incorrect configuration will lead to failures.
- **Scalability:**
- TTS generation can be a bottleneck. Scaling requires adding more queue workers.
- Database performance under load depends on the chosen database and server resources.
- Filesystem performance depends on the chosen disk driver and infrastructure.
- **Error Handling:** While the `GenerateSpeechJob` includes basic failure handling (updating status to FAILED), more sophisticated error reporting or user notification mechanisms might be needed for production. The `TextToSpeechGenerationException` provides a specific exception type.
- **Security:** Assumes standard Laravel security practices are followed. Sensitive credentials (like Google Cloud keys) must be securely managed and not committed to version control. Media URLs generated for cloud storage are temporary and time-limited.
```
While the recommendation is often to keep rules under 500 characters, I find that for complex architectural overviews like this, more detail is necessary. The key is providing the *right* context for the AI. Because this rule has `alwaysApply: true`, its content is included in every interaction with the Cursor agent or Cmd-K AI for this project. Since these rules live in `.cursor/rules`, you can commit them to your repository so the whole team benefits from the same guidance.
Beyond general architecture, I create rules for specific coding standards and preferences. For instance, here’s a rule file detailing some of my conventions for Laravel development:
```
---
description: Laravel project rules
globs: *.php
alwaysApply: false
---
# Laravel project rules
- Every time you have to run an php artisan command, run it with "herd" at the start. Example: `herd php artisan migrate`.
## Models
- Always create models using the artisan command.
- All models use ULIDs instead of big integers for ids as primary key.
- All models should be soft deleted.
## Controllers
- Always create controllers using the artisan command.
- Create controllers following laravel conventions, having index, create, store, show, edit, update, delete method. But only add the methods you need for the specific case.
- When storing or updating, always use form requests for data validation.
- Don't just put all the logic in the controller, use action classes to clearly separate logic.
- Whenever you have to return data to inertiajs or as json, use api resources, and only share the data you need to share.
- Always create feature tests for the controller, mocking the external services.
- Test all the form request validation rules by testing the full request lifecycle, and asserting you got the correct error.
## Form Requests
- Always create form requests using the artisan command.
## API Resources
- Always create api resources using the artisan command.
- When it makes sense, try to add a `dto` method that returns a data object from the request.
## Action classes
- These are simple classes that do only one specific action.
- Put all the action classes under app/Actions directory.
- Use a public `handle` method to execute the action.
- Pass all the necessary parameters to the `handle` method.
- Instead of passing raw arrays use data objects to pass the necessary data to the action class. We use Spatie laravel data package.
- Create feature test for all the action classes that you create.
## Testing
- We use Pest for testing. So, create tests following Pest conventions.
- Everytime you do changes to any php file, you have to run tests to verify everything works as expected.
- Always run mutation tests to verify that the code is fully covered.
- You can run tests with `herd php artisan test --filter=...`.
- You can run mutation tests with the next command `herd coverage ./vendor/bin/pest --mutate`.
- Never use `uses(RefreshDatabase::class)` because it's already added on [Pest.php](mdc:tests/Pest.php) file.
## Formatting
- Make sure you run "./vendor/bin/pint" command, without herd prefix, after you do any change to php files.
```
Notice the `globs: *.php` and `alwaysApply: false` here. This means Cursor will automatically consider applying this rule whenever it's interacting with a PHP file, but it won't always include it. Also, as noted in the rule, some instructions might be very specific to my personal setup (like using herd). If collaborating, it might be better to put such personal preferences in a separate rule file that isn't committed to the shared repository, or perhaps use User Rules (defined in Cursor Settings) which apply globally just for you.
Finally, you'll likely find you need to add more nuanced rules as you work with the AI and see recurring mistakes or patterns you want to enforce. For example, I noticed the AI wasn't always using Laravel's preferred factory relationships when generating tests, so I added a specific testing rule:
```
---
description: Use this rules whenever you are working on Laravel tests
globs:
alwaysApply: false
---
# General Guidelines
- Always use factory relation helpers to create models with relations.
**Example:**
**DO:** `Team::factory()->for($user, 'owner')->create();` or `Project::factory()->for($team)->create();`
**DON'T:** `Team::factory()->create(['owner_id' => $user->id]);
```
This rule uses `alwaysApply: false` and relies on its `description`. The AI will decide whether to include this rule's content based on whether the description seems relevant to the current task (e.g., if it's working on files in the `tests/` directory).
Using these different types of rules allows you to progressively refine how Cursor's AI interacts with your specific project and coding style.
But effective rules rely on the AI having the right information *before* it even starts planning or executing. That brings us to the crucial step of gathering comprehensive project context.
#### Setting the stage: gathering full project context with Repomix
So, how do I actually gather all that code context mentioned in the "Plan" step of my workflow? While there are a few tools for this, my personal preference is the **Repomix CLI**. I find it straightforward for managing different configurations depending on what I need the context for.
Essentially, Repomix bundles up your project's code (respecting things like `.gitignore`) into a single file, and importantly, it lets you embed custom instructions alongside the code.
My setup involves creating a `.repomix` directory at the root of my project. Inside this, I keep my different Repomix configuration files.
For example, when I'm planning a new feature, I use a configuration file I've named `plan.config.json` (located at `.repomix/plan.config.json`). This JSON file tells Repomix things like where to save the output (e.g., `.repomix/output/plan.xml`), what format to use, which files to ignore, and crucially, points to a separate instruction template file (e.g., `.repomix/instructions/plan.md`).
```
{
"output": {
"filePath": ".repomix/output/plan.xml",
"style": "xml",
"parsableStyle": true,
"compress": false,
"headerText": "Custom header text",
"instructionFilePath": ".repomix/instructions/plan.md",
"fileSummary": true,
"directoryStructure": true,
"removeComments": false,
"removeEmptyLines": false,
"topFilesLength": 5,
"showLineNumbers": false,
"copyToClipboard": false,
"includeEmptyDirectories": false,
"git": {
"sortByChanges": true,
"sortByChangesMaxCommits": 100
}
},
"include": ["**/*"],
"ignore": {
"useGitignore": true,
"useDefaultPatterns": true,
"customPatterns": ["./repomix/**", "tmp/", "*.log", "storage/**", ".github/**", "artisan", "bootstrap/cache/**"]
},
"security": {
"enableSecurityCheck": true
}
}
```
This `plan.md` instruction file defines the detailed prompt template for the AI that will create the step-by-step plan. It outlines the role, context, and required output sections (Data Structures, Implementation Steps, Code Snippets, Unit Tests, Verification, Explanation).
```
# Feature Implementation Planning
## Role
You are an AI assistant specializing in code analysis and software feature planning.
## Context
You will be provided with two main inputs:
1. The **complete source code** of a software project.
2. A **specific task** detailing a new feature to be added or an existing feature to be modified (provided below).
## Objective
Your primary objective is to analyze the provided code and the specific task, and then generate a detailed, step-by-step plan to implement the required changes. You **must** use the provided code as the primary context for all suggestions.
## Output Requirements
Your generated plan **must** include the following sections, clearly delineated:
1. **Data Structure Definition:**
- Analyze if the task necessitates the creation of new data structures (e.g., classes, interfaces, structs, database table schemas, configuration objects) or modifications to existing ones.
- If new structures or modifications are needed, clearly define them. Include field names, data types, relationships, and any relevant constraints or default values. Use appropriate code-like notation for clarity (e.g., class definition syntax for the project's language). If no changes are needed, state this explicitly.
2. **Implementation Steps:**
- Provide a numbered list of specific, actionable steps required to implement the feature or modification.
- **1. Prepare Git Branch:**
- Instruct the user to check their current Git branch (e.g., using `git branch` or `git status`).
- Instruct the user that if they are not already on a dedicated feature branch for this task (e.g., they are on `main`, `master`, or `develop`), they **must** create and check out a new feature branch before proceeding. Provide an example command with a placeholder name (e.g., `git checkout -b feature/your-feature-name`).
- Emphasize that all subsequent code changes described in the plan should be committed to this new feature branch.
- **2. [Original Step 1 - e.g., Modify File X]:** (Renumbered) Describe the next logical step...
- **3. [Original Step 2 - e.g., Create File Y]:** (Renumbered) Describe the next logical step...
- _(Continue renumbering and detailing all subsequent implementation steps)_
3. **Code Snippets:**
- For key steps involving code additions or significant modifications, provide relevant code snippets.
- These snippets should illustrate **exactly** what needs to be added or changed. Use placeholders (e.g., `// ... existing code ...` or `/* TODO: Implement complex logic here */`) where appropriate, but provide the essential structure and key lines of code.
- Ensure the snippets adhere to the coding style, conventions, and language/frameworks evident in the provided source code.
4. **Unit Tests:**
- Provide specific unit tests required to verify the correctness of the implemented changes.
- These tests should cover:
- Happy paths (expected successful execution).
- Edge cases.
- Potential error conditions or invalid inputs.
- Write the tests using the testing framework and conventions already established in the project's source code. Include necessary imports, setup, assertions, and teardown.
5. **Verification Instruction:**
- Explicitly instruct the user to run the newly created unit tests _and_ the existing test suite (after committing changes to the feature branch) to ensure the changes work as expected and have not introduced regressions. Specify the command(s) if discernible from the project structure or common practices for the language/framework.
6. **Detailed Explanation:**
- Provide a comprehensive explanation of the proposed changes.
- Clarify **why** this approach was chosen (e.g., leveraging existing patterns, minimizing changes, performance considerations).
- Explain how the new code integrates with the existing system architecture.
- Mention any potential impacts on other parts of the application or any trade-offs made in the proposed solution.
## Important Considerations
- Base **all** recommendations directly on the provided source code.
- Maintain consistency with the project's existing architecture, patterns, naming conventions, and coding style.
- Be precise and unambiguous in your instructions and explanations.
- Ensure the final generated plan is formatted using Markdown.
---
## Here is the specific task you need to plan:
**[PLACEHOLDER: Insert the specific feature request or modification task here.]**
---
```
So, when I run `repomix -c .repomix/plan.config.json`, Repomix generates the `plan.xml` output file containing the code context and the pre-defined instructions. I just replace the task placeholder, and it's ready for the planning AI.
I also have another configuration, say `arch.config.json`, stored in `.repomix/`, pointing to a different instruction file (`.repomix/instructions/architecture-docs.md`). This one prompts the AI to generate architecture documentation based on the codebase.
arch.config.json
```
{
"output": {
"filePath": ".repomix/output/architecture-docs.xml",
"style": "xml",
"parsableStyle": true,
"compress": false,
"headerText": "Custom header text",
"instructionFilePath": ".repomix/instructions/architecture-docs.md",
"fileSummary": true,
"directoryStructure": true,
"removeComments": false,
"removeEmptyLines": false,
"topFilesLength": 5,
"showLineNumbers": false,
"copyToClipboard": false,
"includeEmptyDirectories": false,
"git": {
"sortByChanges": true,
"sortByChangesMaxCommits": 100
}
},
"include": ["**/*"],
"ignore": {
"useGitignore": true,
"useDefaultPatterns": true,
"customPatterns": ["./repomix/**", "tmp/", "*.log", "storage/**", ".github/**", "artisan", "bootstrap/cache/**"]
},
"security": {
"enableSecurityCheck": true
}
}
```
architecture-docs.md
```
## AI Agent Instructions: Architecture Documentation Generation
### Role
You are an AI assistant specializing in code analysis and the generation of software architecture documentation.
### Context
You will be provided with the **complete source code** of a software project.
### Objective
Your primary objective is to analyze the provided codebase and generate comprehensive architecture documentation covering the key aspects listed below. Your analysis and documentation **must** be derived directly from the provided source code.
### Required Documentation Sections
Your generated documentation **must** include the following sections, clearly delineated:
1. **High-Level Overview:**
- Provide a concise summary of the system's primary purpose and its main functionalities as inferred from the code.
- Describe the overall architectural style identified (e.g., monolithic, microservices, client-server, event-driven, layered).
- List the core technologies, programming languages, frameworks, and significant libraries used in the project.
2. **Component Interactions:**
- Identify the major logical or physical components, modules, services, or layers within the system based on the code structure (e.g., directories, namespaces, classes).
- Describe how these components interact with each other (e.g., function calls, API requests, events, shared data stores).
- Where feasible, generate diagrams using Mermaid syntax (e.g., component diagrams, sequence diagrams) to visually represent these interactions. If diagrams are not feasible, provide clear textual descriptions of the communication patterns.
3. **Data Flow Diagrams:**
- Describe the flow of data through the system for key operations or use cases identifiable from the code (e.g., user registration, order processing, data reporting).
- Explain where data originates (e.g., user input, external systems), how it is processed or transformed by different components, where it is stored (e.g., databases, caches, files), and its eventual destination or output.
- Generate diagrams using Mermaid syntax (e.g., flowcharts) to illustrate these data flows, or provide detailed textual descriptions if diagrams are not practical.
4. **Design Decisions and Rationale:**
- Infer significant design decisions based on the code structure, chosen libraries/frameworks, observed design patterns (e.g., MVC, Repository, Singleton), and any explicit comments.
- For each identified decision, attempt to explain the likely rationale behind it (e.g., "The use of framework X suggests a focus on rapid development," "The Repository pattern likely aims to decouple data access logic").
- Clearly state when a rationale is an inference based on common practices versus explicitly mentioned in comments.
5. **System Constraints and Limitations:**
- Identify potential system constraints, external dependencies, or limitations evident from the code.
- Examples include: reliance on specific external APIs or services, assumptions about the deployment environment (e.g., OS-specific calls), potential scalability bottlenecks suggested by algorithms or data handling, areas where the code structure might limit future flexibility.
### Important Considerations
- Base **all** documentation strictly on the provided source code and its structure.
- Use clear, concise, and objective language.
- Structure the output logically with clear headings for each required section.
- **Ensure the final generated documentation is formatted using Markdown.**
- When generating diagrams (Mermaid), ensure they accurately reflect the interactions or flows derived from the code.
- If information for a section cannot be reasonably inferred from the code, state that explicitly.
- Make sure you are using the correct Mermaid syntax for the diagrams. Don't use weird characters like `>` or `<` in the diagrams.
```
Running Repomix with this config (`repomix -c .repomix/architecture-docs.config.json`) produces output perfect for creating that general Cursor Project Rule or for general developer documentation.
It's a good idea to commit the `.repomix` directory containing your configuration (`.json`) and instruction (`.md`) files but add the output directory (e.g., `.repomix/output/`) to your `.gitignore`. And don't hesitate to use AI to help refine these instruction templates!
With a complete context package generated by Repomix, the next step is to feed this into a powerful AI model capable of detailed planning. This is where the large context window models truly shine.
#### The planning phase: leveraging Google AI Studio's context window
So, what do I do with that output file from Repomix, the one containing both the code context and my instructions template? This is where **Google AI Studio** comes into play in my workflow.
The main reason I add this step is to leverage the power of a model like **Gemini 2.5 Pro**. As confirmed by recent information, these models boast a massive context window; often up to **1 million tokens**! This is huge, allowing the AI to process nearly the entire context I generated with Repomix in one go.
And the best part? Using the Google AI Studio web interface itself is generally free for experimentation and development purposes, with generous usage limits.
So, my process is simple:
- Go to Google AI Studio (you can find it at https://aistudio.google.com/ ).
- Start a new chat prompt.
- Paste the *entire* content from the Repomix output file (the code context + the instructions template, with my specific task filled in) into the prompt.
- Run it.
Because **Gemini 2.5 Pro** gets to see the extensive code context provided by Repomix, I find the resulting plan is significantly better (more accurate, more context-aware, and generally a more viable implementation strategy) than if I were to just give instructions directly within Cursor without this deep preparation. It really highlights how crucial providing the right, comprehensive context is for getting good results from these powerful models. Most of the time, the plans it generates are excellent starting points.
Once AI Studio generates the detailed, step-by-step plan (including code snippets, commands, etc., as defined in my Repomix instruction template), I simply **copy that entire plan**.
The next step? Taking that plan over to Cursor and letting its agent do the work.
This structured process significantly boosts productivity, but using AI so heavily also brings important considerations and led me to refine my approach further, adding crucial steps *after* the AI has done its part.
#### Beyond speed: reflections, refinements, and staying in control
There's no doubt this AI-assisted workflow significantly speeds up feature development. However, after using it for a while, I came to an interesting realization. At one point, I felt a bit detached from the codebase because, well, *I* hadn't written large parts of it myself. Initially, I even got frustrated with things like the AI agent adding lots of comments. My first instinct was to try and make it stop.
But then I had a change of heart. I realized those comments, and the act of reviewing the AI's work, were actually *crucial*. Reading through the code, understanding the why behind it, and even cleaning up comments myself forces me to engage with it deeply. It's how I truly learn and internalize the code being added to my project.
So, I've refined my workflow slightly, adding a dedicated review phase **after** the AI agent finishes its work:
- **Understand the AI's 'thinking'**: I go back through the Cursor chat history and carefully examine the changes the AI made. This helps me grasp the solution path it took and any hurdles it encountered.
- **Git is king**: I generally accept all the changes Cursor proposes initially. I don't get bogged down in Cursor's diff interface; I rely on Git as the ultimate source of truth for reviewing changes.
- **Review & refine**: As I review the changes (often using Git tools), I stage the files that look correct. During this process, I'll make small necessary tweaks and remove any comments that aren't adding value.
- **Take notes**: If I spot larger issues or things I want to refactor differently, I jot them down.
- **Test thoroughly**: Once I've staged the main changes, I run all the tests (unit, integration, etc.) multiple times and perform manual checks to ensure everything works and looks right.
- **Commit & iterate**: I commit the reviewed and validated changes. Then, I address the notes I took earlier. Sometimes I make these follow-up changes myself, other times I might prompt Cursor again for specific fixes.
- **Improve the rules**: If I notice the AI consistently making the same kind of mistake, I update my Cursor Project Rules or Repomix instruction templates to prevent it in the future.
Now, I know what some might think: "If you're reviewing everything manually, what's the point of using AI?" It's a fair question. But for me, it's about balance. It's vital that we, as developers, truly know and *own* the code in our projects. This process helps me do that and keeps my skills sharp. And honestly, even with this review step, the overall process is still significantly faster than writing everything from scratch.
**A note for junior developers**: If you're earlier in your career, I highly recommend paying close attention to the *plan* generated by the AI (in the Google AI Studio step). Read through it carefully. You can learn a tremendous amount about different approaches, potential pitfalls, and best practices just by studying the AI's proposed solution. Don't hesitate to ask the AI questions if you don't understand parts of its plan.
##### One last crucial point: security and privacy
A major consideration, especially when working with client projects, is the security and privacy implications of sending code to third-party AI services. Most of us have confidentiality agreements or concerns about proprietary code. It's essential to have an open conversation with your team and clients about what level of code sharing is acceptable. We're still in a bit of a grey area with these technologies, and clear guidelines and decisions are needed on a case-by-case basis.
#### Conclusion: embracing AI assistance mindfully
Navigating the rapidly evolving world of AI coding assistants can feel overwhelming, but the potential benefits for productivity are undeniable. My journey has led me to favor powerful models like **Gemini 2.5 Pro** for their planning capabilities and integrated environments like Cursor for their AI-first features.
The structured **Scaffold -> Plan -> Execute -> Monitor** workflow, enhanced by meticulous context gathering with tools like Repomix and detailed planning via Google AI Studio, has proven incredibly effective for tackling complex Laravel tasks. Guiding the AI with well-defined Project Rules in Cursor further ensures consistency and adherence to standards.
However, speed isn't everything. As I've learned, integrating AI effectively requires a mindful approach. The crucial **review and refinement** step after the AI completes its tasks is non-negotiable for maintaining code ownership, understanding the changes, and ensuring quality. It's about leveraging AI as a powerful assistant, not a replacement for developer skill and judgment.
While challenges remain, particularly around security and the ongoing need to adapt as technology progresses, I firmly believe that embracing these tools thoughtfully allows us to build better software, faster. The key is finding the right balance between automation and active developer involvement, ensuring we remain firmly in control of the code we create.
### Configuring AWS services for Laravel
#### using IAM roles
*Published on May 15, 2025*
---
Laravel provides support for various AWS services such as DynamoDB, SES, S3, SQS, etc. To use these services, it is standard practice to configure access by setting the AWS\_ACCESS\_KEY\_ID and AWS\_SECRET\_ACCESS\_KEY in the .env file. Using access keys is not the most recommended approach for configuring access, as it has some security flaws:
- **Permissions scope**: Our access keys might be incorrectly scoped (e.g. using `AmazonS3FullAccess` permission) allowing for access to more AWS services and resources than required.
- **Access control**: If an attacker retrieves our access keys, they will be able to access your services and resources from a different machine.
- **Traceability**: It is not easy to trace back which user owns which access keys.
In addition to the above, long-lived tokens are considered a security risk because of:
- **Exposure**: If credentials are leaked in version control systems, logs, or shared externally, they can be exploited until explicitly revoked. You may have heard stories about leaked AWS keys where the scammers have created many servers to mine crypto in the exposed AWS accounts.
- **Persistence**: Without rotation policies, these tokens can remain valid for years, creating a wide attack surface for potential breaches.
- **Compliance issues**: Some organizations need to comply with standards like SOC 2, ISO 27001 or HIPAA, which often require minimization of long-lived secrets.
Identity and Access Management (IAM) roles allow us to grant specific permissions (on specific resources) to specific entities. These entities can be users, applications or even other resources. IAM roles follow the principle of least privilege. This means we should grant only the minimum permissions required to the least number of entities.
**Let's explore how IAM roles can be used, using AWS S3 storage as an example:**
Laravel allows us to use various drivers to store our application's files. Using AWS S3 buckets is a popular choice because of how reliable, easy to manage, secure and scalable the driver is.
#### Laravel configuration
Let's create our Laravel application using the Laravel installer. If you already have an application to use, feel free to skip this step
```
laravel new storage-app
```
Once our application is setup, let's install the [Flysystem S3](https://github.com/fsmonter/prompts-mcp) package. Currently, Laravel 12 recommends using the latest version (v3):
```
composer require league/flysystem-aws-s3-v3 "^3.0" --with-all-dependencies
```
The Flysystem package is the file storage library Laravel uses under the hood to interact with S3.
Once we finish the above steps, we can push up the folder to our remote repository for deployment.
#### AWS setup
##### S3 bucket setup
Within our AWS account, let's create an S3 storage bucket. Once we input the bucket name, feel free to leave the other sensible configurations as default. Let's name our bucket test iam-storage-bucket . Our bucket name should be globally unique, not just unique within our S3 account.
##### EC2 setup
Let's create an EC2 instance to host our application. We can use any name and the sensible default configurations. The settings and method we use to create our instance doesn’t change how we will assign the IAM roles. You can also use your already existing instances.
Once our EC2 instance is ready, let's deploy our application as well. This can be through manual deployment scripts or through a server management solution such as Laravel Forge.
Once we've deployed our application, let's configure our .env configuration to use our storage bucket:
```
AWS_BUCKET=test-iam-storage-bucket
AWS_DEFAULT_REGION=eu-north-1
AWS_USE_PATH_STYLE_ENDPOINT=false
```
Let’s not configure the AWS\_ACCESS\_KEY\_ID and AWS\_SECRET\_ACCESS\_KEY environment keys. When they are not included, our application will default to using the IAM role attached to the EC2 instance.
Let's access our tinker shell using php artisan tinker and run the following to test if we can connect to our S3 bucket:
```
Storage::disk('s3')->files();
```
We receive the following error:
```
Reason: Error retrieving credentials from the instance profile metadata service. (Client error: `GET http://xxx.xxx.xxx.xxx/latest/meta data/iam/security-credentials/` resulted in a `404 Not Found` response: xml version="1.0" encoding="iso-8859-1"?>
put('file.txt','Hello, World');
Storage::disk('s3')->files();
Storage::disk('s3')->get('file.txt');
Storage::disk('s3')->delete('file.txt');
Storage::disk('s3')->files();
```
Amazing, right? With the above setup, our S3 bucket is more secure while our EC2 instance has all the required access. We can also apply the same principles if we want to grant read-only access or if we want to prevent deletions on an AWS level.
### Domain-Driven Design is not your folder structure
*Published on May 27, 2025*
---
*DDD isn’t a buzzword, a folder convention, or a badge of architectural maturity. It’s a discipline. And most teams are skipping the hard parts.*
#### The folder fallacy
If Domain-Driven Design could be done by renaming `App/Services` to `App/Domains`, every CRUD app would qualify.
Too often, I’ve seen projects praised for “using DDD” because they have a `Domains/` folder and a tidy directory tree. There’s a `Customer` directory, maybe `Invoice`, `Agent`, `Report`, and that’s where the praise stops. It looks clean. It looks deliberate. But scratch the surface, and you find a system that has no idea what business outcome it serves.
No ubiquitous language. No understanding of the *core domain*. No monitoring for the things that actually drive business value. But hey, it *looks* like DDD.
This illusion is dangerous because it *feels* like progress. The architecture looks different, but the thinking is the same. Developers are still handed specs with no context. Business experts are still siloed. And when failures happen, nobody knows what they’re supposed to protect, because the domain was never modeled, just “foldered”.
**DDD starts with understanding the domain. Folder structure is a side effect, not a starting point.**
##### What it could have been
In a system designed with domain insight, a failure to onboard a customer would be treated as a first-class event. It would be modeled, monitored, and visible across the stack, because the team would understand that customer onboarding is not a UI flow, it’s a business capability.
Instead of just logging API errors, the system would raise a domain alert: “*Customer onboarding completion dropped below threshold X in the past hour.*”
Instead of catching up weeks later, the team would have seen the business risk unfold in real time.
That’s the kind of thinking DDD enables, if it’s done as design, not just decoration.
#### What Domain-Driven Design actually means
DDD isn't a tech stack. It's not a buzzword. And it sure isn't folder hygiene.
At its core, **Domain-Driven Design is about placing the business at the center of your software**. Not the framework. Not the database. Not the "deliverables." It’s a design discipline that forces engineers and domain experts to speak the same language, model what matters, and shape the system around the business, not the other way around.
Yet most teams skip straight to implementation. They confuse *tactical patterns* (like Value Objects and Aggregates) with *strategic design*. Worse, they apply those patterns out of context, mechanically, without understanding *why* they exist.
##### Here’s what DDD really brings to the table:
- **Ubiquitous language**: A shared vocabulary used by everyone, engineers, domain experts, product, QA, that shapes the code, the tests, and the conversations. If you're still translating between "user flow" and "controller logic," you’re not there yet.
- **Bounded contexts**: Clear boundaries where terms, rules, and models make sense locally. The same term (like “customer”) can mean different things in different contexts. DDD doesn’t resolve that ambiguity, it embraces and organizes it.
- **Strategic modeling**: Recognizing which parts of the domain are core (where you differentiate) vs. generic (where you conform). This is what tells you where to invest your design effort, and where to use off-the-shelf solutions without guilt.
- **Collaboration as design**: Good models don’t come from code, they come from conversation. Developers, domain experts, and product must co-design the model. This is not a waterfall handoff, it's an ongoing feedback loop.
If you’re doing DDD and you haven’t had a whiteboard session with someone from finance, legal, or operations, you’re not doing DDD. You’re naming things.
DDD doesn’t promise clean code. It promises **code that means something**, to the business, to the people who run it, and to the teams who evolve it. It’s about building a language and a model that *survives* turnover, scale, and entropy.
And no, your Laravel `Domain/Customer` folder doesn’t get you there.
#### The lost in translation trap
DDD isn’t a design system, it’s a design conversation. And most teams never have it.
Even when teams claim to practice Domain-Driven Design, most of the actual design work happens at the wrong altitude. Conversations occur between product managers and engineering leads. Maybe an architect is there. Diagrams are drawn. Glossaries are written. And then someone writes tickets.
But here’s the problem: **none of those people are the ones actually writing the code.**
And that’s where the model lives.
Not in Confluence. Not in a Figma file. **In the code decisions made at implementation level.**
This is the blind spot in most “DDD” efforts:
- Developers are treated as implementers, not modelers.
- Nuance is lost in translation between “the room” and the repo.
- Assumptions leak into code because the people writing it weren’t part of the conversation that gave the model its shape.
**DDD without developers in the room isn’t design, it’s reenactment.** The ones writing the code need to help shape the model, or it’ll just reflect someone’s guesswork.
Eric Evans put it plainly in the original Domain-Driven Design:
*“A project needs a team that includes both domain experts and developers, working closely together, speaking a common language, and collaborating constantly.”*
And Alberto Brandolini, creator of Event Storming, sharpened the point:
*“It’s not the domain expert’s knowledge that goes into production. It’s the developer’s understanding of that knowledge.”*
##### Design is not a one-time handoff. It’s a loop.
Closing the gap means:
- Running **collaborative modeling sessions** (like Event Storming or Example Mapping) early and often.
- Letting *developers* ask naive questions before they write clever abstractions.
- Building **shared ownership** over terminology, business success metrics, and failure conditions.
Without that, you’re building assumptions, not software. And you’re definitely not doing DDD
#### If you must touch the folder tree…
A folder named `Domain/Customer` doesn’t make your software strategic. It makes it organized, maybe.
There’s nothing wrong with a clean directory layout. Structure helps. Modularity matters. But don’t confuse **structure with design**, and definitely not with Domain-Driven Design.
If you start with folder names like `Domain/Customer` or `Aggregate/Invoice` without first modeling what Customer or Invoice actually mean in your domain, you’re dressing up a CRUD app in architecture cosplay.
It’s easy to fall into the trap of:
- Slicing your app by nouns instead of behaviors.
- Copy-pasting DDD boilerplate from a tutorial repo.
- Nesting files under `Domain/` while still writing logic that doesn’t reflect any shared understanding of the business.
The result? A prettier mess. Still tightly coupled. Still brittle. Still hollow.
##### Folder structure should emerge from the model, not define it.
When done well, structure serves as **an** **artifact of domain understanding**:
- You identify meaningful **bounded contexts**, and each gets its own module or namespace.
- You define **aggregates** with clear transactional boundaries and behavioral consistency.
- You separate core domain logic from supporting or generic subdomains.
That kind of structure? It’s useful. Durable. Legible. But it only makes sense *after* you’ve had the hard conversations.
If you organize before you understand, you’re just naming shelves in a library of fiction.
So go ahead and refactor your folders, but make sure the structure reflects decisions, not aesthetics.
#### Code that means something
Good code isn’t just clean. It’s *correct in context*. That’s what Domain-Driven Design delivers, when you actually do it.
The real value of DDD is long-term clarity.
When the business changes, good domain models don’t break, they adapt. When a new developer joins the team, *they can read the code and understand the business*. When a bug hits production, you don’t just fix the symptom, you trace it back to a concept the whole team understands.
Compare that to the fake DDD setups:
- Where business logic is hidden inside helper functions and service classes with meaningless names.
- Where the same concept is modeled differently in three places, because no one agreed on the language.
- Where folder structures look impressive but collapse under the weight of contradictory assumptions.
That’s not resilience. That’s architectural theater.
##### DDD isn’t about complexity, it’s about intent.
You don’t need to implement every pattern from the blue book to gain value. But you do need to start with what matters:
- Collaborate with the people who know the domain.
- Capture that knowledge in code.
- Use the language of the business *as the language of the software*.
Because at the end of the day, code that means something can be changed with confidence. Code that was just arranged nicely? That’ll rot the same way everything else does.
#### Start where you are
DDD done halfway is better than DDD done as theater.
Smaller steps, done with intent, are more valuable than complex diagrams no one maintains.
Not every project needs, or can afford, a full-scale DDD initiative.
Maybe you're building a smaller app. Maybe you don’t have a dedicated domain expert. Maybe you’re a two-person startup shipping fast. That’s fine.
You don’t need aggregates, factories, and a CQRS pipeline just to sell concert tickets or track deliveries.
But here’s what you can do, starting *today*, that’s more valuable than a fancy folder structure:
- **Talk to someone who knows the domain**. Ask how they think about success, exceptions, edge cases. That’s the beginning of the model.
- **Use their language in your code.** Not your own. Not your framework’s.
- **Draw the boundary.** Even one meaningful bounded context is better than none. Call it out. Protect it.
- **Model behaviors, not just data.** Code should say what the system does, not just what it stores.
- **Track what matters.** If you can’t tell when your core domain is failing, you're not done modeling.
These small steps, done with care, *are* Domain-Driven Design.
Not the full book. But the right spirit. And that's enough to build something resilient, meaningful, and ready to grow.
Because in the end, DDD is not a checklist.
It’s a way of thinking. And you don’t need a big system to start thinking clearly.
#### A Toolset Full of Buzzwords
You’ve heard the names: Aggregate. Factory. CQRS. Value Object. They show up in talks, blog posts, and architecture diagrams like some elite club of patterns.
Truth is, they're just tools. And like any tool, they only matter if they solve a problem you actually have.
Here’s a quick primer on what they mean, in plain terms, and why they might matter to you.
##### Aggregate
- **What it is**: A group of objects treated as a single unit of consistency. One object (the “root”) controls changes to the whole.
- **Why it matters**: It helps enforce business rules and transactional boundaries without leaking logic across the app.
- **When to use it**: When multiple entities are tightly coupled and should always be updated together.
Example: An `Order` with `LineItems`. You don’t change the items directly, you go through the `Order`, which ensures everything stays valid.
##### Factory
- **What it is**: A way to encapsulate the creation of complex domain objects.
- **Why it matters**: It keeps object construction logic out of your domain logic and prevents duplicated setup code.
- **When to use it**: When creating a domain object involves rules, default values, or multiple dependencies.
Example: A `CreateInvoiceFactory` that builds an invoice with customer-specific tax rules and sequential invoice numbers.
##### CQRS (Command Query Responsibility Segregation)
- **What it is**: A pattern that separates reads (queries) from writes (commands).
- **Why it matters**: It allows reads and writes to evolve independently, performance, consistency, and modeling can be tuned on each side.
- **When to use it**: When your domain grows complex and the read side has very different needs than the write side.
Example: A reporting dashboard needs fast, denormalized data. The underlying domain model handles strict consistency on write. CQRS gives you both.
##### Value Object
- **What it is**: An object defined by its value, not its identity. Immutable. Replaces primitive types with meaningful concepts.
- **Why it matters**: Makes code more expressive and enforces rules at the type level.
- **When to use it**: Whenever a concept deserves more structure than a raw string or int.
Example: `EmailAddress`, `Money`, or `DateRange`. They carry meaning and enforce their own validity.
##### Repository
- **What it is**: An abstraction that lets you retrieve and persist aggregates without exposing persistence details.
- **Why it matters**: Keeps domain logic clean and persistence separate.
- **When to use it**: When working with aggregates that shouldn't be tightly coupled to the database layer.
Example: A `CustomerRepository` that returns full `Customer` aggregates ready to apply domain logic, not just hydrated ORM records.
You don’t need to memorize these. You don’t even need to use them all.
But when your app starts pushing back, when logic can get messy, when rules may leak, when change gets risky, these are the tools that help DDD scale.
They’re not meant to intimidate you. They’re here to support you. Reach out to them when they solve a problem. Not before.
Use them like a carpenter uses a chisel, not because it looks fancy, but because it makes the cut clean.
### Process is dead. Long live process.
*Published on July 24, 2025*
---
#### The death of process
The spec was followed. The work was delivered. But when it was reviewed, the goalposts had moved.
What was once signed off as the right approach was now "not what we wanted." The team pulled up the documentation. It matched the implementation precisely. But it didn’t matter, because the definition of done was never shared, never stable, and never protected by structure.
This wasn’t a failure of engineering skill. It was a failure of process, because there wasn’t one. Not a real one, anyway.
There were no tickets. No backlog. No incident protocol. Just Slack threads and good intentions. A system of "just ask in chat," "ping the right person," or "somebody should probably look at that." It felt lean. Unencumbered. Agile, even, in the lowercase sense of the word.
But when things broke, nobody knew what should happen next. Because under the hood of this "lightweight" approach was an unacknowledged truth: even anti-process is still a process. It's just one no one agreed on, documented, or maintained.
In place of structure, we had improvisation. In place of shared understanding, we had folklore. And in place of ownership, we had diffusion masked as flexibility.
Process didn’t die because we formalized it too much. It died because we pretended we didn’t need one.
#### Mythologies of the anti-process culture
Every team has encountered leadership that’s skeptical of formal process. Often brilliant, sometimes battle-hardened by years of shipping under pressure, they’re certain that *real work* doesn’t need rituals.
They believe in hiring smart people and letting them figure it out. They trust in grit over governance, Slack over systems, and “just get it done” over “let’s make it repeatable.” Their teams are told they’re agile, but without Scrum, Kanban, or any of the artifacts that might imply bureaucracy.
This perspective doesn’t come from negligence, it often comes from experience. They’ve seen process done poorly. They’ve watched teams suffocate under frameworks meant to liberate. So they discard process entirely, assuming that less is more.
On the surface, this looks like empowerment. In practice, it’s mythology.
**Myth #1: Process slows us down**
The idea here is that structure is the enemy of speed. And it’s true, bad structure does slow things down. But so does chaos. When every deployment relies on tribal knowledge and direct pings, speed becomes luck.
**Myth #2: Smart people don’t need process**
Smart people especially need process, because they’re the ones solving hard problems. And solving hard problems repeatedly, safely, and collaboratively requires clarity, not just intelligence.
**Myth #3: Agile is a scam**
Some leaders saw Agile done poorly, boxed creativity, forced ceremonies, outsourced thinking to tooling, and concluded the entire idea was snake oil. But Agile wasn’t meant to be a product. It was a mindset. Rejecting Agile because of Jira is like rejecting music because you hate Spotify playlists.
**Myth #4: We’re small, we don’t need it**
Small teams *especially* need scaffolding. Structure doesn’t have to mean overhead, but it does have to exist. Without it, visibility fades and failure detection becomes luck.
Skepticism isn’t the problem. Lack of exposure to healthy process is. Without that exposure, teams keep reinventing the same dysfunctions, and calling them flexibility.
##### The cult of the living spec
In some teams, process isn’t absent, it just lives entirely in one person’s head.
There’s a document somewhere. Maybe a slide. Maybe a doc link buried in chat. But it’s not a source of truth, it’s a snapshot of what that person believed in that moment. And when that belief shifts, the spec shifts with it. Retroactively.
This isn’t iteration. It’s revisionism.
The danger of pseudo-process isn’t lack of vision, it’s lack of integration. Input happens in silos. Feedback is downstream. Execution becomes a guessing game: not “what did we build wrong?” but “what will they decide it *should have been* now?”
There are no rituals because the belief is that good people don’t need them. No documentation, because “I already explained it.” No change logs, because “that’s not how I think anymore.”
This kind of system doesn’t just break delivery, it breaks trust. Engineers disengage. They stop investing in quality because it feels temporary. Product managers become crisis mediators. Reviews become defensive instead of collaborative.
The worst part? From the outside, it still looks productive. There’s a spec. There’s Slack activity. There’s something being delivered.
But the product doesn’t evolve. It pivots reactively, led not by insight but by impulse. And the team doesn’t grow, they survive.
Real process isn’t about democratizing every decision. But it *is* about creating continuity, accountability, and shared understanding, so that what gets built isn’t just a reflection of one person’s current mood.
#### The aesthetic of productivity
When a team lacks real process, it often compensates with something else: the *aesthetic* of productivity.
Messages fly. Threads are active. There’s a constant hum of effort. It feels like progress, but it’s motion, not momentum.
Presence becomes performance. Engineers check in frequently, update reactively, context-switch constantly, not because it’s effective, but because it *looks* like engagement.
Behind the noise, core issues remain:
- Ownership is unclear.
- Work is duplicated or lost.
- Decisions happen in DMs and vanish into ether.
This isn’t agility. It’s latency disguised as alignment.
Real process doesn’t suppress hustle. It removes the need for reactive hustle in the first place. It trades urgency for flow.
#### What real process looks like
Real process isn’t a ritual. It’s a system. And it looks like this:
**1. Work is visible**
Everyone should know what’s being worked on, what’s blocked, and what’s next. A single source of truth beats ten Slack threads.
**2. Ownership is explicit**
Accountability isn’t about blame, it’s about having the authority to fix what breaks, and the space to do it.
**3. Feedback loops exist, and they’re short**
Bugs surface quickly. Metrics shift observably. Alerts go somewhere useful. You know when you’re drifting *before* it becomes an outage.
**4. Rituals have a purpose, or they die**
If a standup is just a status dump, kill it. If retros change nothing, stop holding them. Process isn’t sacred. Outcomes are.
**5. The team can describe how it works**
Ask your team how work is prioritized, how releases happen, how bugs escalate. If the answers are inconsistent, you don’t have a process, you have folklore.
#### Rebuilding from first principles
Good process isn’t installed. It’s constructed by asking what the team needs to do great work reliably.
**Principle 1: Make failure detectable**
If your system hides failure, it’s broken by design.
**Principle 2: Assign clear ownership**
Nothing should depend on memory or influence. Someone must own every critical flow.
**Principle 3: Protect deep work**
Context switching is a symptom of structural failure. Async over meetings. Prioritization over chaos.
**Principle 4: Measure what matters (quietly)**
Data isn’t for surveillance. It’s for course correction. Collect real signals, not vanity metrics.
**Principle 5: Iterate the process, not just the product**
Processes rot when they stop evolving. Schedule the retrospective, for the system, not just the sprint.
#### Process as a shield
There’s a quieter role process plays, one that doesn’t show up in charts or dashboards: it protects people.
Without structure, teams don’t just inherit ambiguity, they inherit personality. Direction shifts based on emotion. Priorities are driven by influence, not impact. In the absence of clarity, the loudest voice wins.
This isn’t a rare dysfunction. It’s the default state when process is absent or performative. Decisions get made in private. Blame flows downhill. Engineers start optimizing for optics, not outcomes, because what matters most is being on the right side of whoever’s in charge that week.
A real process insulates teams from this.
- It defines priorities *before* pressure can distort them.
- It formalizes escalation paths so people don’t get cornered in DMs.
- It clarifies ownership so success or failure doesn’t hinge on politics.
- It distributes power so accountability is shared, not weaponized.
Without process, every developer becomes a negotiator. Every PM becomes a translator. And every bug becomes a potential battleground.
Good process doesn’t eliminate conflict, but it *contains* it. It channels disagreement into structured forums. It slows down knee-jerk reactions. It gives individuals the psychological safety to focus on the work, not on surviving the workplace.
In short: process doesn’t just protect delivery. It protects dignity.
##### When success is silent
The irony of process is that its greatest achievement is often invisibility. When systems work, incidents still happen, but their impact is controlled. When alignment is strong, blockers resolve before they escalate. When ownership is clear, priorities don’t get tangled in politics.
But in many organizations, only failure gets documented. Only outages get retrospectives. Only breakdowns prompt leadership attention. So even if a team quietly ships a feature that saves thousands of users, or recovers thousands of leads, the moment passes without reflection.
Success, without process to capture and amplify it, becomes disposable. Invisible. Forgotten.
This imbalance creates a warped lens: the one failure defines the team, while the hundreds of quiet wins fade into the background. The result? A culture of reactivity, not resilience.
Real process fixes that.
- It tracks impact, not just incident count.
- It creates feedback loops that capture value, not just damage control.
- It forces pause, not just to fix, but to *acknowledge*.
- It builds memory, so future decisions are made with perspective, not panic.
Without this, leadership becomes myopic. The last outage is all that matters. The wins aren’t shared because they weren’t seen. And over time, teams lose their sense of progress, because no one ever stops to say: *this worked*.
#### Long live process
Process isn’t dead.
What’s dead is the bloated, performative parody of it. The kind that confuses tools for truth, ceremonies for alignment, and frameworks for thinking. What’s dead is the belief that if you just install the right rituals, good software will fall out the other side.
But process, the real kind, the kind that aligns smart people around hard problems, is not just alive. It’s essential.
It’s what lets small teams move quickly *without burning out*. What lets complex systems evolve *without collapsing*. What allows organizations to learn *without repeating failure.*
Process isn’t the enemy of speed. Bad process is. And no process is just invisible bad process.
So we don’t throw away structure, we strip it down. We don’t reject Agile, we reject how it’s been marketed. We don’t resist accountability, we demand clarity.
Good process:
- surfaces reality instead of obscuring it,
- scales autonomy instead of centralizing control,
- reduces risk instead of creating overhead,
- and evolves with the team instead of stifling it.
If you’re building something that matters, something that lives beyond a single engineer’s head, then you need process. Not as a cage, but as scaffolding. Not as dogma, but as discipline.
Because when it’s done right, process doesn’t feel like process.
It feels like momentum.
### Build an MCP server with Laravel Loop
*Published on August 13, 2025*
---
Our Laravel apps have useful features like model management, data processing, business rules, and report generation. But when working with Claude or Cursor, none of those features are available. AI agents can't see your model’s data, can't call your APIs, and can't leverage any of your application's capabilities.
Laravel Loop changes that by turning your Laravel functionality into tools that AI assistants can discover, understand, and use.
#### Laravel Loop and MCP
[Laravel Loop](https://github.com/kirschbaum-development/laravel-loop) is a powerful Model Context Protocol (MCP) server: a standard that lets AI agents discover and invoke tools from external systems. With Laravel Loop, you can serve Laravel functionality through MCP by creating tools.
Here's what an interaction with an MCP server looks like:
Claude discovered MCP tools defined in the server, understood what it does from its definition, and called it.
#### The application
To show how this works, let’s use a real Laravel application containing a prompt library that manages 200+ curated AI prompts for content analysis, writing, and data extraction.
The idea is to retrieve and use these prompts while chatting with an agent.
We’ll cover how to create tools for discovering and composing prompts with our content. The same patterns apply whether you’re managing prompts, products, users, or any other resource.
The [full application code](https://github.com/fsmonter/prompts-mcp) is available here. To install Laravel Loop, check the [setup instructions](https://github.com/kirschbaum-development/laravel-loop?tab=readme-ov-file#installation) here.
#### Building your first tool
The simplest tool transforms any logic into something AI can use:
```php
namespace App\Mcp;
use Kirschbaum\Loop\Tools\CustomTool;
CustomTool::make('list_prompts', description: 'Get a complete list of all available prompts with name and category.')
->using(function () {
$prompts = Prompt::active()
->orderBy('title')
->get(['name', 'title', 'category']);
$result = "## Available Prompts ({$prompts->count()} total)\n\n";
return $result . $prompts->map(function ($prompt) {
return <<name}
Title: {$prompt->title}
Description: {$prompt->description}
Category: {$prompt->category}
MD;
})->join("\n");
);
```
That's it! We can now ask an LLM, “What prompts do you have?" and get a list directly from the Laravel app.
#### Adding parameters
Tools will often need parameters. Here’s how to make AI assistants pass the right data:
```php
CustomTool::make(
name: 'search_prompts',
description: 'Search for prompts by keyword in title, description, or name. Returns prompts from all sources ready to use.',
)
->withStringParameter(
name: 'query',
description: 'Search query to find prompts',
required: true
)
->withStringParameter(
name: 'limit',
description: 'Maximum number of results to return (default: 10)',
required: false
)
->using(function (string $query, string $limit = '10') {
$prompts = Prompt::active()
->public()
->where(function ($q) use ($query) {
$q->where('title', 'like', "%{$query}%")
->orWhere('description', 'like', "%{$query}%")
->orWhere('name', 'like', "%{$query}%");
})
->orderBy('title')
->get();
if ($prompts->isEmpty()) {
return "No prompts found matching '{$query}'.";
}
$result = "## Search results for '{$query}' ({$prompts->count()} found):\n\n";
return $result . $prompts->map(function ($prompt) {
return <<name}
**Title**: {$prompt->title}
**Description**: {$prompt->description}
**Usage**: `compose_prompt` with prompt_name `{$prompt->name}`
MD;
})->join("\n");
});
```
Now LLMs can understand requests like:
- “Find prompts for analyzing content”
- “Show me writing prompts”
- “Search for summarization tools”
The AI assistant figures out the parameters automatically based on the tool definition and the parameter descriptions.
#### Using existing logic
You can use existing logic in your tools, or if you need more complex logic, extract it into separate classes:
```php
CustomTool::make(
name: 'compose_prompt',
description: 'Apply a prompt template to your content for analysis or processing'
)
->withStringParameter('prompt_name', 'Name of prompt to use')
->withStringParameter('content', 'Your content to process with this prompt')
->withStringParameter('additional_context', 'Extra context or instructions', required: false)
->using(function (string $prompt_name, string $content, string $additional_context = '') {
$prompt = Prompt::active()->where('name', $prompt_name)->first();
// Resolve any existing class
$composedPrompt = app(PromptService::class)->compose(
prompt: $prompt,
inputContent: $content,
additionalContext: $additional_context
);
return "EXECUTE THIS PROMPT: " . $composedPrompt;
});
```
Now you can ask the assistant: "Use the analyze\_claims prompt on this @article.md" and it will automatically:
- Find the right prompt
- Compose it with the passed-in content
- Return the ready-to-execute prompt
- Execute the analysis
#### Registering the toolkit
To expose the tools to the server, we can group them into toolkits:
```php
namespace App\Mcp;
use Kirschbaum\Loop\Collections\ToolCollection;
use Kirschbaum\Loop\Contracts\Toolkit;
class PromptLibraryToolkit implements Toolkit
{
public function getTools(): ToolCollection
{
return new ToolCollection([
$this->createListPromptsTool(),
$this->createSearchPromptsTool(),
$this->createGetPromptDetailsTool(),
$this->createComposePromptTool(),
$this->createListCategoresTool(),
]);
}
// Tool definitions...
}
```
And then make the Toolkit available to the MCP server in AppServiceProvider:
```php
use Kirschbaum\Loop\Facades\Loop;
use App\Mcp\PromptLibraryToolkit;
public function boot(): void
{
Loop::toolkit(new PromptLibraryToolkit());
}
```
#### Seeing it in action
With the toolkit implementation registered, we can now connect it to an MCP client and start using the tools.
##### Transports explained
Laravel Loop supports these [MCP transport methods:](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)
**STDIO transport**
STDIO transport executes your Laravel application as a subprocess, ideal for local development.
**HTTP+SSE transport**
Server-Sent Events transport operates over HTTP with persistent connections. You can use it for local development, and it is ideal for remote servers in production environments.
##### Configure the MCP client
Generate the MCP configuration for your client by running this command and following the instructions:
```
php artisan loop:mcp:generate-config
```
This command creates configuration you paste into your AI client settings. Once connected, your AI client lists all available tools automatically:
Claude code:
Cursor:
With the MCP server connected and running, you can start having conversations with the assistant, and it will have access to all the tools.
#### Conversation example
Here’s what a real conversation with the prompts library looks like:
The AI discovers tools, calls them with the right parameters, and uses the results - all while having a natural conversation.
##### Last thoughts
We covered how to define tools and serve them through MCP with Laravel Loop. By implementing the patterns demonstrated, you can create intuitive tools for any AI agent of your preference.
I hope this post gives you an idea of the power of MCP. Connecting Laravel applications to AI models opens endless interaction possibilities for AI-assisted applications and development tooling.
To name a few ideas, imagine:
- Sharing code components between projects
- Advanced code refactoring using team-defined patterns
- Well-formatted and structured documentation generation and maintenance
- E2E testing generation
The possibilities are vast, and I believe we don’t yet fully understand its potential.
Thanks for reading! Give Laravel Loop a try, and discover what’s possible.
##### Additional Resources
- [Laravel Loop Documentation](https://github.com/kirschbaum-development/laravel-loop)
- [Model Context Protocol Specification](https://modelcontextprotocol.io/)
- [Prompt Library Application](https://github.com/fsmonter/prompts-mcp)
### Drop in comments for Filament with Commentions
*Published on August 27, 2025*
---
Building commenting systems from scratch is one of those features that sounds simple until you actually start implementing it. You need comment models, user mentions, reactions, permissions, real-time updates, and a clean UI that fits your admin panel. What starts as "just add comments" quickly becomes a complex feature that takes weeks to build properly.
If you're using Filament for your admin panel, you already know the incredible power of this framework. Filament has revolutionized Laravel development by providing beautiful, full-stack components that accelerate development while maintaining the elegance and flexibility Laravel developers love. With the recent release of Filament v4 (officially stable as of August 2025), the framework has reached new heights with significant performance improvements, unified schemas, and enhanced flexibility that make building sophisticated admin interfaces even more effortless.
Now, with Commentions - fully compatible with both Filament v3 and the cutting-edge v4 release - you can extend that same elegance to commenting systems. As Filament's official development agency partner, Kirschbaum Development has created a drop-in commenting solution that feels like it was built into Filament from day one.
Instead of building all of this from scratch, wouldn't it be cool if you could just do something like this?
```
// In your Filament InfoList
CommentsEntry::make('comments')
->mentionables(fn (Model $record) => User::all())
```
Or add it as a simple table action:
```
// In your Filament table
->recordActions([
CommentsAction::make()
->mentionables(User::all())
])
```
This is exactly what [Commentions](https://github.com/kirschbaum-development/commentions) brings to your Filament application - a complete commenting system built with deep understanding of Filament's patterns and philosophy.
#### Why we built Commentions
As Filament's official development agency partner, Kirschbaum Development is dedicated to elevating the experience of working with Filament. With Dan Harrin as a member of our team and our commitment to supporting Filament's growth, we're uniquely positioned to understand what developers need most.
Filament has transformed how we approach Laravel development. Its component-driven architecture, intuitive APIs, and stunning design system have made building complex admin panels not just faster, but genuinely enjoyable. The recent stable release of Filament v4 has taken this even further, delivering impressive performance improvements (up to 3x faster for large datasets), unified schemas that seamlessly combine forms, infolists, and layout components, and support for custom table data that opens up entirely new possibilities for dynamic interfaces.
Through our work on large, complex Filament projects - leveraging everything from Filament's powerful table builders to its flexible form components - we found ourselves building similar commenting features repeatedly across client applications. Each time, we'd create comment models, build mention systems, handle permissions, and design interfaces that perfectly matched Filament's aesthetic excellence. We realized we were solving the same problem over and over again - and if we were facing this challenge, so were countless other developers in the thriving Filament community.
Our partnership with Filament means we're not just building packages - we're crafting solutions that seamlessly extend Filament's philosophy of elegant, powerful components. With full compatibility for both Filament v3 and the groundbreaking v4 release, Commentions embraces the framework's evolution while maintaining the simplicity and power that makes Filament so exceptional. Whether you're leveraging v4's new unified schemas or sticking with the proven patterns of v3, Commentions adapts to provide the same exceptional experience.
#### Getting started
Installation is straightforward - just require the package and publish the migrations:
```
composer require kirschbaum-development/commentions
php artisan vendor:publish --tag="commentions-migrations"
```
Next, implement the required interfaces on your models:
```
// User model
use Kirschbaum\Commentions\Contracts\Commenter;
class User extends Model implements Commenter
{
// Your existing user model
}
// Any model you want to add comments to
use Kirschbaum\Commentions\HasComments;
use Kirschbaum\Commentions\Contracts\Commentable;
class Project extends Model implements Commentable
{
use HasComments;
}
```
That's it for the setup. Your models are now ready to support comments.
#### Multiple ways to add comments
One of Filament's greatest strengths is its flexibility - the framework adapts to your workflow rather than forcing you into rigid patterns. Commentions embraces this philosophy by offering multiple integration options that work seamlessly with Filament's component system.
##### In InfoLists (recommended)
Leveraging Filament's powerful InfoList components, the cleanest approach is adding comments directly to your resource views:
```
Infolists\Components\Section::make('Comments')
->schema([
CommentsEntry::make('comments')
->mentionables(fn (Model $record) => User::all()),
]),
```
##### As table actions
Filament's table builder is incredibly versatile, and with v4's enhanced performance and support for custom data, it's become even more powerful. Commentions integrates beautifully for quick commenting directly from your table views. The package automatically detects your Filament version and uses the appropriate action format:
```
// Filament v4 (latest)
->recordActions([
CommentsAction::make()
->mentionables(User::all())
])
```
```
// Filament v3 (still supported)
->actions([
CommentsTableAction::make()
->mentionables(User::all())
])
```
##### As header actions
Perfect for dedicated comment management, taking advantage of Filament's consistent action patterns:
```
protected function getHeaderActions(): array
{
return [
CommentsAction::make(),
];
}
```
#### Smart mentions and events
One of Commentions' most powerful features is its mention system. When users mention someone in a comment using @username, the package automatically dispatches events you can hook into:
```
// Listen for mentions
use Kirschbaum\Commentions\Events\UserWasMentionedEvent;
class SendUserMentionedNotification implements ShouldQueue
{
public function handle(UserWasMentionedEvent $event): void
{
$event->user->notify(
new UserMentionedInCommentNotification($event->comment)
);
}
}
```
The package also dispatches events for new comments and reactions, giving you complete control over notifications and integrations.
#### Real-time updates with polling
Modern users expect real-time updates, and Filament's thoughtful architecture makes this effortless to implement. With v4's performance optimizations and partial rendering capabilities, real-time features are more efficient than ever. Commentions leverages Filament's built-in polling capabilities to deliver live comment updates:
```
CommentsEntry::make('comments')
->poll('10s') // Poll every 10 seconds
->mentionables(User::all())
```
This integration showcases what makes Filament exceptional - complex functionality becomes simple configuration. The polling works seamlessly with Filament's lifecycle and, with v4's enhanced performance optimizations, runs more efficiently than ever. Comments refresh automatically without requiring page refreshes or complex JavaScript, while v4's partial rendering ensures minimal resource usage.
#### Advanced customization
##### Custom comment models
Need additional fields or logic? Extend the base Comment model:
```
class CustomComment extends \Kirschbaum\Commentions\Comment
{
protected $fillable = ['body', 'author_id', 'commentable_id', 'commentable_type', 'priority'];
}
```
Then update your config:
```
// config/commentions.php
'comment' => [
'model' => \App\Models\CustomComment::class,
],
```
Flexible permissions
The package includes sensible defaults for permissions, but you can override them completely:
```
class CommentPolicy extends CommentionsPolicy
{
public function create(Commenter $user): bool
{
return $user->can('create-comments');
}
public function update($user, Comment $comment): bool
{
return $user->id === $comment->author_id || $user->isAdmin();
}
public function delete($user, Comment $comment): bool
{
return $user->id === $comment->author_id || $user->isAdmin();
}
}
```
##### Custom display names and avatars
Filament's commitment to beautiful, consistent interfaces extends to how users are represented, and v4's unified schemas make component integration even more seamless. Control how users appear in comments by implementing Filament's standard interfaces:
```
use Filament\Models\Contracts\HasName;
use Filament\Models\Contracts\HasAvatar;
class User extends Model implements Commenter, HasName, HasAvatar
{
public function getFilamentName(): string
{
return $this->full_name ?? $this->email;
}
public function getFilamentAvatarUrl(): ?string
{
return $this->avatar_url;
}
}
```
This approach demonstrates Filament's excellent design philosophy - consistent interfaces that work across the entire ecosystem.
#### Beyond comments: activity streams
Sometimes you want to show more than just user comments. Commentions supports "renderable comments" - custom items that appear in the comment stream:
```
public function getComments(): Collection
{
// Get status changes
$statusHistory = $this->statusHistory()->get()->map(
fn (StatusHistory $history) => new RenderableComment(
id: $history->id,
authorName: $history->user->name,
body: sprintf('Status changed from %s to %s', $history->old_status, $history->new_status),
createdAt: $history->created_at,
)
);
// Merge with actual comments
$comments = $this->comments()->latest()->with('author')->get();
return $statusHistory->merge($comments)->sortByDesc('createdAt');
}
```
This creates a unified activity stream showing both comments and system events.
#### Real-world impact
The difference Commentions makes becomes clear when you compare implementation approaches. This comparison also highlights why Filament has become so beloved in the Laravel community, especially with the recent v4 release bringing even more power and performance:
##### Without Commentions (traditional approach)
Building a commenting system from scratch requires handling multiple complex pieces:
- Multiple database tables and migrations for comments, mentions, and reactions
- Custom Livewire components that match Filament's sophisticated styling
- Manual mention parsing and validation logic
- Permission systems scattered across controllers and policies
- Custom CSS to achieve Filament's beautiful, consistent aesthetic
- Event dispatching infrastructure for notifications and integrations
- Real-time update handling with WebSockets or polling mechanisms
- Form builders that integrate seamlessly with Filament's patterns
- Responsive design that works across Filament's breakpoints
- Testing suites for all the custom functionality
##### With Commentions + Filament's power
```
CommentsEntry::make('comments')
->mentionables(User::all())
->poll('30s')
```
This transformation exemplifies what makes Filament extraordinary - it turns complex requirements into elegant, readable code. With v4's performance improvements and unified schemas, this elegance comes with even better performance and flexibility. Commentions extends this philosophy, providing enterprise-grade commenting functionality through Filament's intuitive component system.
#### Embracing Filament's excellence
Commentions embodies both the Laravel philosophy of making complex things simple and Filament's commitment to beautiful, intuitive interfaces. As developers who have witnessed Filament transform from an innovative idea into an essential tool for Laravel development, we're continually amazed by its impact on our productivity and code quality. The recent stable release of Filament v4 represents a major milestone, bringing substantial performance improvements, unified schemas, and enhanced flexibility that push the boundaries of what's possible with admin interfaces.
Filament's component-driven architecture, thoughtful APIs, and attention to detail have set a new standard for admin panel development. With v4's 3x performance improvements for large datasets, seamless integration of forms and infolists through unified schemas, and support for custom table data, the framework has evolved to handle even the most demanding applications while maintaining its signature elegance.
The package handles the hard parts - mention parsing, event dispatching, permission checking, and UI consistency - while giving you hooks to customize behavior for your specific needs. Every component follows Filament's conventions, every style matches Filament's aesthetic excellence, and every interaction feels natural within the framework. With full support for both Filament v3 and v4, you can confidently upgrade your Filament installation knowing that Commentions will continue to work seamlessly.
Our partnership with Filament ensures that Commentions isn't just another package - it's a first-class citizen in the Filament ecosystem. Whether you're building a project management tool, a content management system, or any application where users need to collaborate through comments, Commentions provides the foundation you need without compromising the elegance that makes Filament special.
Give it a try in your next Filament project. Your users will appreciate the familiar, polished commenting experience that matches Filament's high standards, and your development team will appreciate the time saved on a feature that "just works" - built by developers who live and breathe Filament every day, with deep appreciation for the framework that has revolutionized Laravel development.
### AI and the developer skillset
*Published on October 9, 2025*
---
AI-assisted programming, while still nascent, is rapidly becoming central to how software developers think about and create applications. As this transformation unfolds, much of the focus has been on navigating an uncertain and volatile technical environment. While AI is certainly reshaping this foundational area, it’s worth reflecting upon whether the most significant change may be happening in an entirely different domain.
#### Previous releases
The past can often offer insight during times of upheaval or metamorphosis. No paradigm shift is precisely analogous to another, but the recent emergence of AI echoes several critical moments in software development history:
- The arrival of high-level programming languages in the 1950s-1960s introduced more efficient frameworks for writing software and managing machine-level details.
- The personal computer revolution and GUI development of the 1980s democratized computing capabilities and completely transformed how we design and distribute code
- The internet's rise in the 1990s created entirely new software verticals while permeating every aspect of daily life.
AI carries elements of all these transitions simultaneously, serving as a new way to tame software complexity, a democratizing force, and a fundamental shift that touches everything we build.
These historical disruptions reveal a generally consistent pattern: they force developers to refactor their skillsets by revising problem-solving approaches, expanding technical fluency to new tools and technologies, and updating practices in areas like security, quality assurance, and system design to address emerging challenges and risks.
In many ways, as we are learning, AI's impact on the software developer role does follow this familiar template. There are, however, aspects that set it apart qualitatively from any past occurrence.
#### Breaking changes ahead
Beyond providing unprecedented opportunities to build faster and at greater scale, AI enables something far more transformational: ***software creation through delegation and outcome-focused everyday human language.*** Although the evolution of the field has always been a story of increasing abstraction, if previous advances were baby steps towards this end, AI is closer to a quantum leap.
For the developer, the modern software development process is largely a hands-on exercise in translating general stakeholder requirements into specific algorithms, design patterns, and syntax. We bridge the gap between inexact, ambiguous ordinary language requests and precise, deterministic computer instructions, having a foothold in both the declarative and the imperative.
AI disrupts this process by moving developers as a default to the same abstraction level as our stakeholders. That is, we too are reasoning about and expressing requirements using ordinary language (through the ever-present LLM prompt). What’s more, we’re also actively offloading implementation details to autonomous decision-making proxies. This creates cognitive distancing between us and that **natural and critical translation step** where so often ***what we're building*** and ***for whom*** finally emerges with clarity.
Joel Spolsky described the problem over 20 years ago, noting “Code generation tools which pretend to abstract out something, like all abstractions, leak, and the only way to deal with the leaks competently is to learn about how the abstractions work and what they are abstracting. So the abstractions save us time working, but they don’t save us time learning.”[1](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/) More recently, Addy Osmani, reflecting on the developer impact of AI, observed, "the higher-level the abstraction, the harder it is to specify how exactly the software should work."[2](https://newsletter.pragmaticengineer.com/p/how-ai-will-change-software-engineering)
While abstraction challenges may not be unfamiliar, the current AI-driven pervasiveness is, along with the potential implications for developer competencies.
As AI increasingly shifts our work toward that which is system-oriented, conceptual, and verbal, we may find ourselves operating more deeply in the realm of human needs, intent, and language. This environment will require us, perhaps more than ever, to continue bridging the gap between stakeholder requests and effective technical solutions; however, *success in this environment* will likely demand that we strengthen our human-centric, human-only capabilities: emotional intelligence, empathy, collaborative problem-solving, clear communication, and ethical reasoning, among other traditional “soft skills”. These competencies may not have always been recognized as essential for being perceived as a great developer, but they have always been what's required to build truly great software.
Ultimately, the full impact of AI on our skillset, technical and non-technical alike, remains uncertain. There are more questions than answers, but as Alan Kay reminds us, “The future is not laid out on a track. It is something that we can decide, and to the extent that we do not violate any known laws of the universe, we can probably make it work the way that we want to.”[3](https://www.cs.uni.edu/~wallingf/blog/archives/monthly/2004-11.html#e2004-11-06T21_03_42.htm)
### Human oversight in AI-assisted development
*Published on February 4, 2026*
---
During our annual company retreat, Kirschbaum developers organized a hackathon around a concept that has dominated technical discourse over much of the past year: vibe coding. Originally coined by computer scientist and OpenAI co-founder Andrej Karpathy, the term describes a specific and unconventional approach to AI-assisted programming:
*There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists.[1](https://x.com/karpathy/status/1886192184808149383)*
Vibe coding, like all AI-assisted programming, relies on natural, results-oriented language to generate application code; however, unlike the far more measured and deliberate AI-assisted programming approach that many of us employ daily, vibe coding offloads nearly all technical reasoning and technical output to the AI itself. In other words, AI doesn't simply help with development, *it essentially becomes the developer.*
In an effort to better understand the contours and boundaries of AI-assisted development, our hackathon challenged teams of three to build the most feature-rich application possible in only two hours using a strict vibe coding methodology. Teams then evaluated each other's creations in a surprise peer review session.
What we discovered not only reinforced the capabilities and constraints of AI as a programming tool, but also served as a powerful reminder about the perennial value of human involvement in software development.
#### What worked with vibe coding?
Our vibe coding experience kindled genuine enthusiasm around AI for many of us. As part of the developer toolkit, AI *is* quite remarkable, and can even be transformative for certain types of tasks. Teams identified several areas where vibe coding delivered advantages to the development process.
- **AI excelled at generating predictable boilerplate code.** The most consistent finding was that AI dramatically accelerated the process of project bootstrapping. Teams rapidly generated database migrations, models, foundational UI elements, and other scaffolding that typically consumes meaningful development time, but often does not require exceptional developer skill. This advantage helped to reduce cognitive and task load, allowing teams to concentrate on high-value and differentiating aspects of their applications, such as feature implementation and user experience.
- **AI meaningfully reduced time-to-MVP and facilitated rapid prototyping.** The teams frequently identified the speed in which they were able to produce a mostly functional MVP as one of the most compelling abilities of leveraging AI: "The amount of things AI can generate with a single prompt is unbelievable." In comparison to a traditional programming approach, the throughput and velocity of AI-generated code allowed for additional iteration and refinement cycles and helped mitigate “analysis paralysis” and over-engineering.
- **AI flattened the learning curve and encouraged exploration.** While most teams worked with familiar technologies like Laravel, others opted to build using new tools and frameworks. What they found was that they were able to achieve a working application faster and with more ease than conventional learning paths would allow.
#### What didn’t work with vibe coding?
Despite real advantages, vibe coding came with substantial difficulties, many of which only emerged as projects progressed. Initial efficiency gains often gave way to bewilderment as teams grappled with fundamental and often critical application issues. We identified several areas where vibe coding was limiting or counterproductive to the development process.
- **AI struggled to produce cohesive user interfaces.** AI-generated interfaces frequently suffered from visual inconsistencies, usability problems, and various rendering bugs. As one team member noted, "Our application had several visual issues (incorrectly handled wrapping, width constraints, overlapping buttons, etc.) that would not have been present typically." While AI could generate individual UI components quickly, it was often unable to create a refined, holistic user interface.
- **AI output was frequently incomplete or deficient.** Teams observed that it was not uncommon for AI to only partially implement critical functionality and features, often without explanation or warning. Even where issues around functionality were not present, AI-generated code often lacked the cohesion, structure, and modularity expected by experienced developers.
- **AI code included obvious security risks and privacy violations.** The most concerning and frequently cited challenge was the prevalence of security and privacy vulnerabilities in AI-generated code. Teams identified a range of problems, including hardcoded secrets, incomplete authentication flows, inadequate user data isolation, and other conspicuous attack vectors.
#### Why does it matter?
Taken in isolation, many of our vibe coding findings are hardly surprising. The power and limitations of AI-generated code are already well-documented. The value of our experience wasn’t so much, then, in discovering new failure modes or efficiency gains, but in the broader perspective it offered on what it means to be a software professional in an age of intelligent tools.
By deliberately embracing an unfamiliar and extreme approach through vibe coding, our hackathon reinforced a fundamental insight that can be easy to miss when using AI in a more practical, everyday manner: human involvement is not some bottleneck to be optimized away, but rather the very backbone that gives AI-assisted development direction and meaning.
Ownership, accountability, and expert oversight in software by humans is essential not only for practical considerations like security, privacy, quality, and maintainability, but also for those concerns that often lie between stated requirements: judgment, context, empathy, and understanding.
While AI can accelerate the *how* of building software, it cannot replace the *why, what,* or *for whom.* In representing the margins of AI-assisted development, vibe coding highlights where humans still matter most, reminding us that successful, safe, and responsible software creation will always depend on human-machine collaboration over human-to-machine abdication.
### Why we love React Native and Expo
*Published on May 12, 2026*
---
For years, building a mobile app meant a hard choice: hire a separate team and maintain a separate codebase, or wrap a website in an app shell and call it good. For shops already invested in modern web tooling, that duplication is expensive and frustrating.
We favor tools that help teams move efficiently without sacrificing maintainability. For teams already invested in modern web tooling, React Native and Expo can be an efficient path to native mobile development. They let us build native mobile apps using the same language and patterns our web team already works with every day. Here's what we like about them.
#### From web to native
The biggest draw of React Native for a web-focused team is that it's just JavaScript and TypeScript. The components, hooks, and state management our team writes every day carry over without much friction. TThe platform changes, but many of the underlying development patterns remain familiar, so web developers tend to ramp up on mobile quickly.
That familiarity carries into the tooling. Expo Router gives you file-based routing on mobile. If you've worked with Next.js or Nuxt, it's the same idea: your file structure defines your navigation. Create a file, get a screen.
For styling, we use Uniwind to bring the Tailwind experience to React Native. Tailwind is already a core part of our web workflow, so styling mobile components feels normal from day one. Same utility classes, same mental model, applied to a native canvas.
#### Native applications from a shared codebase
A common misconception about cross-platform frameworks is that they're just websites in an app wrapper. React Native isn't that. Every component you write maps to a real native UI primitive. A `` becomes a `UIView` on iOS and an `android.view` on Android.
Expo UI takes this further. It was introduced in SDK 53 and reached beta in SDK 55, and it lets you use real SwiftUI and Jetpack Compose components directly inside an Expo app.
The idea is that you define a button once and it renders as a native iOS button on Apple devices and a native Android button on Android. Each platform gets the UI it expects, from a single codebase. It's still in beta, but worth keeping an eye on.
#### What Expo brings
If React Native is the engine, Expo is the framework on top of it. Think of it like Laravel for PHP or Next.js for React. Expo handles much of the surrounding tooling and infrastructure so teams can stay focused on product development.
##### Starting with Expo Go
When you create an Expo project, you can scan a QR code and have it running on a real device in minutes. No Xcode, no Android Studio, no Apple Developer account. Expo Go is a sandbox app that hosts your JavaScript bundle, which makes it great for prototyping and early exploration.
##### Moving to development builds
Expo Go has limits. It can only run native libraries that are already bundled into it, and it doesn't accurately simulate things like push notifications, deep linking, or OAuth flows.
That's when you move to a development build. It's your own custom version of Expo Go, compiled for your app, containing exactly the native code your project needs. For any production-quality app, this is the recommended path. You get the full fidelity of a real device test before you ever submit to a store.
##### The Expo SDK and custom native code
Expo ships with a large SDK of pre-built native modules covering most things you'd need: camera, location, notifications, file system, sensors, biometrics, and so on. For many applications, teams can avoid writing native platform code entirely.
When a library doesn't exist for some specific need, the Expo Modules API gives you a way out. You can write Swift and Kotlin and expose those capabilities to JavaScript without leaving the Expo workflow. Your project stays maintainable even when custom native work shows up.
##### EAS for builds and over-the-air updates
Expo Application Services (EAS) is the cloud layer that makes shipping and maintaining a mobile app a lot less painful. EAS Build automates compiling your app and submitting it to the App Store and Google Play.
EAS Update is especially useful for teams accustomed to rapid web deployment cycles.. It lets you push JavaScript changes (bug fixes, copy tweaks, small UI adjustments) directly to users' devices, skipping the app store review queue. This reduces operational friction around smaller releases and fixes.
#### A mature open source community
React Native is one of the most widely used cross-platform mobile frameworks in the world, and the open source community around it is active. That means there's a third-party library for almost anything you'd want to do, and the framework itself keeps getting better.
Expo is also open source, and it's become the default way most teams build React Native apps today.
#### Conclusion
React Native and Expo are a practical answer to a real problem: how do you build native mobile apps without doubling your team or your codebase? You lean on what your web team already knows (JavaScript, TypeScript, Tailwind-style styling, file-based routing) and pair it with a mature framework and a solid deployment platform. The result is mobile work that ships quickly and stays maintainable as it grows.
If you're thinking about a mobile project and want to talk through whether React Native and Expo are the right fit, we'd love to speak with you.
### Lead Web Application Developer
### **Seeking a lead/senior developer**
Kirschbaum Development Group is a software engineering firm focused on solving interesting and complex business problems. We're unique because we are a developer led and developer driven company. Our customers range from small startups to some of the largest companies in the world.
- We are official Laravel partners.
- We make on-the-clock contributions to open source.
#### **What is important to us**
We are a small team of highly skilled people. Aside from will, dedication, and ambition, our biggest asset is our ability to learn and grow. People don't pay us because we have all the answers. They pay us because we solve problems quickly and effectively. We embrace modern technology as well as tried and true development practices and principles from the past.
We bring cautiousness and stability to our work. We are honest, hardworking, and passionate. We value people and relationships over processes and tools. We view ourselves as master crafters who respect and honor the importance and significance of what we do.
We believe that happy people make better decisions, write better code, and make great products. Team members will stay with us longer when they feel valued, feel like what they do matters, and feel like they have a say in the process.
We value personal and professional growth and prefer to work with people who have shown they will be invested in and committed to their own growth as individuals and professionals.
#### **Job description**
We are seeking a Lead Developer / Team Lead to work on a variety of projects ranging from SaaS products and ecommerce sites, to building out APIs and architecting backend tools. Candidate works remotely.
In addition to the details outlined below, a successful candidate will have expert level understanding and experience with Object Oriented Programming, will be self motivated, have a love for learning and new technology, and be able to lead and mentor others.
The ideal candidate will be a full-stack web application developer (talented on both frontend and backend) who has a knack for problem solving and critical thinking. We are looking for someone with experience architecting solutions and breaking business requirements down into achievable technical tasks.
#### **We offer great benefits**
- Work from anywhere
- Great work life balance
- Competitive salary
- Company computer
- Paid vacation
- Budget for books/conferences
- Paid holidays
- Year-end bonus
- Health, dental & vision insurance
- Paid open-source time
- 401k with company match & profit share
- Parental leave
#### **Responsibilities**
- Keeps the team laser focused on our objectives and helps to resolve any issues that may arise.
- Show excellent judgement in architecting and planning applications and features at all stages of the project lifecycle.
- Interact effectively with clients to determine project needs and develop solutions.
- Attention to detail and the ability to consistently deliver quality work.
- Takes overall accountability for keeping the project clean and organized. This includes identifying next steps, clarifying unclear tickets, and breaking larger tickets into smaller and more manageable tasks.
- Takes professional accountability for getting things done. Does whatever it takes to make things happen, or gives advance warning if things aren't going well so we can arrange for help.
- Understands successful concepts in UI, UX, design cleanliness, and design balance, and takes appropriate action to resolve situations where minimum standards are not being met.
- Takes overall accountability for the quality of code we write on a project.
- Takes overall accountability for the stability of a project.
- Ensures all team members on the project are clear on their tasks and have sufficient work for the day/week.
- Runs project meetings when a delivery manager / scrum master is not on meetings.
- Reviews code carefully and provides constructive feedback.
- Responsible for keeping a production deployment path clear, and ensures that they are not a bottleneck for deployments.
#### **Qualifications**
- Experience in a lead developer / team lead role, including:
- 5+ years experience with PHP
- 4+ years experience with Javascript (experience with a javascript framework such as Vue.js or React preferable)
- 4+ years experience with Laravel
- 4+ years experience with application design/architecture
- Advanced understanding of HTML and CSS
- Familiar with Agile development methodologies
- Advanced experience/knowledge of database architecture and design
- History/knowledge of successful development workflows
- History/knowledge of successful team workflows
- Strong critical thinking skills
- Good communication and writing skills
- An eye for detail and a commitment to quality work
- Bachelor's degree preferred
- Previous customer service and client communication experience preferred
- Must be able to work Eastern Standard Time hours of 9 a.m. to 5 p.m.