Git And Collaboration Workflows
Git Fundamentals for PHP Project Files
Git records changes to project files. In PHP work, that means source code, tests, Composer metadata, configuration examples, migrations, and documentation should be tracked, while generated files, dependencies, caches, logs, and secrets should usually be ignored.
The key skill is knowing which files belong in the repository and which files are local or generated. That affects reviews, deployments, onboarding, and debugging.
Track source, not generated output
A PHP project usually tracks files like these:
track:
- src/OrderTotal.php
- tests/OrderTotalTest.php
- composer.json
- composer.lock
- phpstan.neon
- phpcs.xml
- README.md
ignore:
- vendor/
- .phpunit.cache/
- storage/logs/app.log
- .env
The tracked files let another developer install, run, test, and review the project. The ignored files are recreated locally or contain machine-specific state.
Composer files
composer.json describes dependencies and scripts. composer.lock records the exact installed package versions for an application.
For most PHP applications, commit both:
composer.json
composer.lock
Do not commit vendor/. The dependency code should be installed from Composer.
vendor/
If you are working on a reusable library rather than an application, the lock-file convention can vary. Follow the project's existing practice.
Environment files
Real credentials and local environment values should not be committed.
.env
.env.local
Projects often commit an example file:
.env.example
The example file documents required keys without exposing secrets.
Caches and logs
Test caches, framework caches, log files, uploaded temporary files, and generated reports should normally stay out of Git.
.phpunit.cache/
var/cache/
var/log/
storage/logs/
storage/framework/cache/
coverage/
Some frameworks need empty directories to exist. In that case, projects sometimes commit a placeholder file such as .gitkeep inside the directory while ignoring generated contents.
Check the current state
Use git status before and after changes.
git status --short
Short status helps you see what files are modified, added, deleted, or untracked.
Before committing, inspect the actual diff.
git diff
This is where you catch accidental debug output, unrelated formatting, secrets, or generated files.
Stage intentionally
Do not blindly stage every file. Stage the files that belong to the change.
git add src/OrderTotal.php tests/OrderTotalTest.php
If a generated file appears in git status, update .gitignore or leave the file untracked. Do not commit it just because it appeared.
A useful PHP .gitignore shape
This is not universal, but it shows the common categories.
vendor/
.env
.env.local
.phpunit.cache/
coverage/
var/cache/
var/log/
storage/logs/
Always adjust for the framework and project.
What to remember
Git should capture the source of truth for the project, not every file produced while running it. Track code, tests, configuration, lock files for applications, and documentation. Ignore dependencies, caches, logs, local secrets, and generated output.
Before moving on, make sure you can explain why composer.lock is usually committed for applications, why vendor/ is ignored, and why .env.example is safer than committing .env.
Project File Policy In Real PHP Work
A Git repository should let another developer reconstruct the project from source, not from one developer's machine. That is why PHP applications usually commit application code, tests, migration files, Composer metadata, lock files, configuration examples, static assets that are authored by the team, and documentation. The repository should not depend on a local vendor/ directory, a private .env, a test cache, or a build artifact that was produced by an unrecorded command.
The important distinction is source of truth versus local result. composer.json and composer.lock are source for an application dependency set. vendor/ is the installed result. A database migration is source for a schema change. A local SQLite database created while testing is a result. A Vite entrypoint or stylesheet written by the team is source. A minified bundle may be source only when the deployment process explicitly requires committed build output; otherwise it should be produced by CI.
File policy should be written down because teams inherit different framework defaults. Laravel, Symfony, WordPress, legacy custom applications, and static-site exports all create different temporary directories. A useful .gitignore is not a pile of copied patterns; it documents categories: dependencies, caches, logs, local secrets, generated reports, uploaded test files, and editor state. Keep comments when a pattern is surprising.
Lock Files And Reproducibility
For applications, committing composer.lock makes installs reproducible across developers, CI, and deployment. It records exact package versions selected from the constraints in composer.json. Without it, two developers can run composer install at different times and receive different transitive dependencies while believing they are reviewing the same change.
Reusable libraries can follow a different convention because library consumers resolve dependencies in their own applications. Even there, many projects keep a lock file for their own CI. Follow the repository's policy and document the reason. Do not delete a lock file as a drive-by cleanup because another project you saw did it differently.
When the lock file changes, review it. A one-line change to composer.json can update many packages if the command was composer update. For a security update that intentionally changes one dependency, prefer a targeted update and make the diff explainable. The lock file is part of the review, not background noise.
Secrets And Local Configuration
.env.example belongs in Git because it documents required keys and safe defaults. .env normally does not, because it contains credentials, hostnames, tokens, and machine-specific values. If a secret is committed accidentally, removing it in a later commit is not enough; the secret has entered history and must be rotated.
Use pre-commit secret scanners and remote secret scanning where available, but treat them as backups. The primary control is clear file policy and review discipline. When a new integration is added, update the example configuration and onboarding docs so developers do not invent local files with undocumented names.
Generated Files And Deployment
Some projects commit generated output deliberately. A package may commit generated parser tables because consumers should not need the generator. A legacy hosting environment may deploy only committed files. If the project has such a rule, state the generator command, input files, expected output, and review criteria. Generated output without a reproducible command becomes unreviewable drift.
For modern PHP applications, CI usually installs dependencies and builds assets from source. In that model, committing vendor/, cache directories, coverage reports, or built bundles increases merge conflicts and hides the actual source change. Review git status --short before every commit and ask why each untracked file exists.
Reviewing File Additions
A good file-policy review asks: Is this file authored or generated? Does it contain a secret or local path? Can CI recreate it? Is it required for tests or deployment? Does it belong in a package, application, or environment-specific layer? Will committing it create noisy diffs later?
Use git diff --stat to notice unexpectedly large artifacts, and git diff --name-only to inspect categories. If a framework requires empty directories, commit a placeholder and ignore the contents. If a path mixes source and generated output, refine the ignore rules so the intended source remains visible.
After this lesson, you should be able to decide which PHP project files belong in Git, explain the role of Composer lock files, keep secrets and local state out of history, document intentional generated files, and review a status output before staging changes.
Auditing An Existing Repository
When joining an existing PHP project, audit file policy before making broad cleanup changes. Run git ls-files to see what is already tracked and compare it with git status --ignored --short to understand ignored paths. A file already tracked remains tracked even if a later .gitignore pattern matches it. Removing tracked generated files should be a deliberate commit with a clear explanation, not a hidden side effect inside a feature branch.
Look for recurring noise in recent history: cache files repeatedly changed, built assets updated without source changes, local paths in configuration, or lock-file updates unrelated to dependency work. Those patterns tell you where repository hygiene is costing review time. Fix one category at a time, and include the command that regenerates any file you stop committing.
Also check deployment assumptions. Some older PHP projects deploy by copying the Git checkout directly to a server, while newer pipelines install dependencies and build artifacts in CI. The correct tracking policy depends on that path. If deployment currently relies on a committed artifact, change the deployment process before deleting the artifact from Git.
Safe Cleanup Workflow
A safe cleanup starts with a branch that changes only repository policy. Update .gitignore, remove wrongly tracked generated files with git rm --cached when the files should remain locally, and run the test or build command that proves the project still works from a fresh checkout. Document any local command developers must run after pulling the cleanup.
For secrets, use a different process. If a real secret entered history, rotate it first, then remove references and consider history-rewrite policy only with repository owners. The practical security fix is invalidating the exposed credential; a tidy diff alone does not protect anything.
During review, treat new top-level files as architecture changes. A new config file, script, generated directory, or lock file can alter how every developer installs and deploys the project. Ask who owns it, which command updates it, and whether it belongs in the application, package, or local environment layer. That small pause prevents repository policy from changing accidentally through an otherwise ordinary feature branch. Use the same care for deletions.
Practice
Task: Sort Tracked And Ignored Files
Sort these files into track and ignore groups for a typical PHP application.
src/OrderTotal.php
tests/OrderTotalTest.php
composer.json
composer.lock
vendor/autoload.php
.env
.env.example
.phpunit.cache/test-results
storage/logs/app.log
phpstan.neon
Requirements
- Put each file in exactly one group.
- Explain why
vendor/should not be committed. - Explain why
.env.exampleis different from.env. - Keep the answer focused on PHP project work.
Show solution
src/OrderTotal.php
tests/OrderTotalTest.php
composer.json
composer.lock
.env.example
phpstan.neon
Ignore:
vendor/autoload.php
.env
.phpunit.cache/test-results
storage/logs/app.log
vendor/ should not be committed because Composer can install dependencies from composer.json and composer.lock.
.env can contain real local secrets and machine-specific values. .env.example is safe to commit because it documents required keys without storing real credentials.