Five decisions inside every PHP application
Requirements become a data model
We start by listing the nouns: customer, site, job, visit, invoice. Then come the awkward questions. Can a job belong to two sites? Is a canceled invoice deleted or kept? Who is the customer when an agency books for its client? The answers become tables, relationships and status fields, and we review that model with you before design begins. Getting it right at this stage is cheap. Changing it after a year of live data is the most expensive kind of change request there is.
Access rules checked in one place
Every application ends up with more roles than planned. We define permissions as small named abilities, such as "approve quotes" or "export customers", group them into roles, and check them in one place in the code. Two rules matter most. Permission is enforced on the server for every request, never only by hiding a menu item. And it is checked per record: being allowed to view invoices does not mean being allowed to view another company's invoice by changing the number in the address bar.
Why slow work leaves the page request
PHP serves each request from a worker process with a time limit, and a server has a fixed pool of those workers. A report that runs for two minutes inside a page request ties one up, and a few impatient users clicking again can exhaust the pool for everybody. So anything slow, or anything that calls an outside service, is recorded as a job and picked up by a separate command-line worker. The user gets an immediate acknowledgment and a notification when the job finishes. Jobs are written so that running one twice does no harm, since retries are a fact of life. On modest hosting the queue can be a jobs table drained by cron. Either way, failures have to be visible to an admin and not buried in a log file.
Files, reports and the slow creep of data
Uploads never sit in a public folder. They are stored privately and streamed through a controller that checks who is asking, with size limits set on purpose in the PHP and web server configuration before a client meets them by accident. Spreadsheet imports are read in chunks so a large file cannot exhaust the PHP memory limit. Reports are where applications slow down as tables grow, so we write them against indexes we planned for, paginate everything, and move heavy summaries to nightly tables when live queries stop being quick.
Testing and getting to production
The main paths of each role are covered by automated feature tests: sign in, create, approve, export. Rules with money or dates in them get PHPUnit tests with the edge cases spelled out, and PHPStan checks the whole codebase for type errors. The same pipeline builds the release. Composer installs the locked dependencies, migrations run, the opcode cache is reset and traffic switches to the new release folder, with the previous one kept for rollback. Most of these applications sit on a framework, usually through our Laravel application development team, and a public or mobile interface is added through PHP API development when it is needed.