Secondary constructors in PHP.

Alexander Bondars
3 min readJun 18, 2024

--

Why do we need secondary constructors?

Secondary (or “multiple”) constructors allow us to add extra logic to the new object creation process. They not only add new logic but also allow us to limit or extend the number of initial arguments. Unfortunately, PHP doesn’t have secondary constructors at the language level, as Kotlin does, but we can emulate this behavior using private constructors and static methods instead.

The main advantage of using “multiple” constructors is experienced when you need to create a new object in a consistent state depending on certain conditions and want to avoid numerous void arguments for the constructor. I like to use this technique with Models (Entities) in my projects.

For better understanding, I will show you a simple example of how secondary constructors can simplify your life.

Let’s imagine you need to create a KYC Questionnaire for your clients, and the group of required fields in this questionnaire may differ depending on the user’s employment status:
Employed:
- Status: employed;
- Income source;
- Annual income range;
Self employed:
- Status: self_employed;
- Annual income range;
Retired:
- Status: retired;
- Annual income range;
Unemployed:
- Status: unemployed;

Of course, you can rely on the classic way of object creation and implement the model in any desired state, just skipping non-usable arguments with some default arguments:

final class Questionnaire 
{
public function __construct(
private EmploymentStatus $employmentStatus,
private ?string $incomeSource,
private ?array $annualIncomeRange,
){
}

// ... other methods ... //
}

$employed = new Questionnaire(EmploymentStatus::EMPLOYED, 'Company Name', [60000, 80000]);
$selfEmployed = new Questionnaire(EmploymentStatus::SELF_EMPLOYED, null, [50000, 60000]);
$retired = new Questionnaire(EmploymentStatus::RETIRED, null, [20000, 30000]);
$unemployed = new Questionnaire(EmploymentStatus::UNEMPLOYED, null, null);

But this approach also allows us to break the rules and create unemployed persons with income, retired — with company information and employed without income and company info at all.

$unemployed = new Questionnaire(EmploymentStatus::UNEMPLOYED, null, [20000, 30000]);
$retired = new Questionnaire(EmploymentStatus::RETIRED, 'Company name', null);
$employed = new Questionnaire(EmploymentStatus::EMPLOYED, null, null);

Yes, sure, you can create extra rules inside the constructor to cover all the cases and validate the input depending on the EmploymentStatus, but this will overload the constructor with extra business logic and hide requirements under a pile of conditions and “if” statements. This complexity can be avoided by using a simpler approach.

Secondary constructors.

The simplest way involves using a private constructor and public static methods that control how this object can be created:

final class Questionnaire 
{
private function __construct(
private EmploymentStatus $employmentStatus,
private ?string $incomeSource,
private ?array $annualIncomeRange,
){
}

public static function asEmployed(string $incomeSource, array $annualIncomeRange): self
{
return new self(
EmployementStatus::EMPLOYED,
$incomeSource,
$annualIncomeRange
);
}

public static function asSelfEmployed(array $annualIncomeRange): self
{
return new self(
EmployementStatus::SELF_EMPLOYED,
null,
$annualIncomeRange
);
}

public static function asRetired(array $annualIncomeRange): self
{
return new self(
EmployementStatus::RETIRED,
null,
$annualIncomeRange
);
}

public static function asUnemployed(): self
{
return new self(
EmployementStatus::UNEMPLOYED,
null,
null
);
}

// ... other methods ... //
}


$employed = Questionnaire::asEmployed('Company name', [50000, 80000]);
$selfEmployed = Questionnaire::asSelfEmployed([40000, 60000]);
$retired = Questionnaire::asRetired([30000, 50000]);
$unemployed = Questionnaire::asUnemployed();

Thus, you give the developer no choice but to follow the rules and create a consistent Model with the correct set of data for each business logic case. This approach reduces possible errors and, moreover, brings additional business information about the Questionnaire into the code itself. Now, instead of searching through conditions in the code, you can gain an understanding of the business context just by looking at the method name and required arguments.

Try this method in your own projects to see the benefits.

Happy coding!

--

--