Someone suggests a submodule the first time two apps need the same code. It sounds like sharing; what it actually does is pin a commit of one repository inside another. Sometimes that is exactly what you want. For PHP code, it usually is not.
Why they have a reputation#
A submodule is a second repository with its own history, and Git only records which commit it sits on. That design creates the daily friction:
- A plain
git cloneorgit pulldoes not fetch submodule contents - the directory sits empty untilgit submodule update --init --recursive. - The submodule’s HEAD is detached by default, so committing inside it needs the same care as any detached state.
- Updating means committing in the submodule, pushing it, then committing the new pin in the parent - two commits, two pushes, and a parent repo that is wrong if either is forgotten.
- CI, deploy hooks and new teammates each get to discover all of this once.
The Laravel answer for shared code#
If the shared thing is PHP - a domain package, SDK wrappers, shared DTOs - make it a Composer package instead. A private package on Satis or Private Packagist, or even a path repository while it is small, gives you real versioning: composer update, semver constraints, and a lock file that says exactly what runs where. That is strictly better than a pinned commit with no version number, and every Laravel developer already knows the workflow.
When the submodule is right#
The honest cases:
- Shared non-PHP content - infrastructure definitions, deployment config, a docs site - where Composer cannot carry it anyway.
- Vendoring a third-party project you patch and need to track against upstream, where forking plus subtree is heavier than the pin.
- You genuinely want an exact, reviewable pin with no registry involved: the parent repo’s history shows every bump as a normal commit.
If you take one on#
git clone --recurse-submodules git@yourserver:app.git
git config submodule.recurse true
The clone flag populates the submodule from day one; the config makes ordinary pulls update it too. Both remove the class of mistake that gives submodules their name, because the empty-directory surprise is the one people hit first.
The rule of thumb: PHP code becomes a package; everything else is allowed to be a submodule - with the flags set before the first teammate clones it.
Share this post
Keep reading
Annotating Commits with git notes: Metadata Without Rewriting History
Some things belong on a commit but not in it: review links, benchmark results, the ticket nobody remembered to reference. git notes adds them after the fact, no rebase required.
Backing Up Your Git Server So It Actually Restores
Every clone is a partial backup - but hooks, config and server state live outside the repo. Mirror, bundle, copy off-site, and rehearse the restore.
Cherry-Picking: One Commit Without the Branch
Cherry-pick copies a single commit onto your branch and leaves the rest behind. Right for hotfixes across release branches, wrong as a sync strategy.