Trait conflict resolution rules — in-depth
Concept
Traits in PHP allow code reuse in single-inheritance languages. When two traits define the same method name, PHP requires you to explicitly resolve the conflict — or a fatal error occurs.
Conflict rules:
- Two traits, same method name: Fatal error at class definition time unless resolved.
- Resolution syntax:
insteadof— choose which trait's method to use.as— alias the discarded method under a new name (and optionally change visibility). - Alias doesn't remove the original:
TraitA::method as aliasNameaddsaliasNameas an alias pointing toTraitA::method, but the conflict still exists — you still needinsteadofto resolve it.
Trait and abstract class: A trait can require that the using class implements abstract methods. abstract protected function getName(): string; inside a trait — the using class must define it.
Trait and constructor: Traits should NOT define __construct — it leads to confusion about initialization order. The exception: if only one class uses the trait and you document the dependency carefully.
insteadof chooses ONE, as aliases the OTHER: Standard pattern:
use TraitA, TraitB {
TraitA::hello insteadof TraitB; // use TraitA's hello
TraitB::hello as helloFromB; // alias TraitB's hello
}Visibility change with as: as can also just change visibility without aliasing:
use MyTrait { myMethod as private; }Trait property conflicts: Two traits with the same property name AND same default value are merged (no error). Different default values → fatal error. This is stricter than method conflicts.
Code Example
<?php
trait Logging
{
public function log(string $msg): void
{
echo "[LOG] {$msg}\n";
}
public function handle(): void
{
echo "Logging::handle\n";
}
}
trait Timestampable
{
public function handle(): void // SAME method name as Logging!
{
echo "Timestampable::handle\n";
}
abstract protected function getTableName(): string; // required from using class
}
// ❌ Fatal error — conflict not resolved
// class Model { use Logging, Timestampable; }
// ✅ Resolved with insteadof and as
class UserModel
{
use Logging, Timestampable {
Logging::handle insteadof Timestampable; // use Logging's handle
Timestampable::handle as stampHandle; // alias Timestampable's handle
log as private logInternal; // change visibility of log()
}
protected function getTableName(): string { return 'users'; } // satisfies abstract
}
$model = new UserModel();
$model->handle(); // "Logging::handle"
$model->stampHandle(); // "Timestampable::handle" (via alias)
$model->log('test'); // "[LOG] test"
// $model->logInternal('x'); // ERROR — now private
// Visibility change only (no rename)
trait HasSecret
{
protected function secret(): string { return 'shh'; }
}
class PublicSecretHolder
{
use HasSecret {
secret as public; // promote from protected to public
}
}
$obj = new PublicSecretHolder();
echo $obj->secret(); // 'shh' — accessible as public
// Abstract requirement from trait
trait Validatable
{
abstract protected function rules(): array;
public function validate(array $data): bool
{
foreach ($this->rules() as $field => $rule) {
if ($rule === 'required' && empty($data[$field])) return false;
}
return true;
}
}
class CreateUserRequest
{
use Validatable;
protected function rules(): array // must implement this (abstract from trait)
{
return ['name' => 'required', 'email' => 'required'];
}
}