Constants vs. String Literals: Crafting Cleaner PHP Code.

Grzegorz Lasak (Gregster)
10 min readSep 6, 2023

--

In the programming, especially in long-term projects, details do matter when it comes to clarity and maintainability of our code.
One such shift, often overlooked yet profound in its impact, is the choice between using String Literals and Constants.

At first, it might seem like stylistic preference but when we look deeper into that matter, we’ll find that this choice has far-reaching implications, affecting everything from code readability to refactoring ease.

In this article, we’ll embark on a journey through the PHP landscape, exploring the advantages of constants and understanding why they’re the secret ingredient to crafting cleaner, more robust code.

The Case Against String Literals

As we experience on daily basis, using string literals is just easy, fast and does not cause any additional complexity.
We use them to represent f.ex. keys in associative arrays, column names, filenames etc.

All seems great but once our project grows, our OOP approach becomes more complicated or when we write more tests, we might run into annoying issue while refactoring things (well, the ones who are not coding masters at least).

Let’s have a look at very basic code (done specifically to present some of the issues):

<?php

namespace App;

class MyStringLiteralExposeClass {

public function getArrayWithNameAndType(): array
{
return [
'name' => 'Cool Name',
'type' => 'CoolType'
];
}

}

class MyStringLiteralConsumerClass {

public function __construct(
protected string $name,
protected MyStringLiteralExposeClass $myStringLiteralExposeClass = new MyStringLiteralExposeClass()
) {}

public function displayNameAndType(): void
{
$nameTypeArray = $this->myStringLiteralExposeClass->getArrayWithNameAndType();
echo sprintf(
'The name provided in construct is %s, and extracted name %s which has type %s',
$this->name,
$nameTypeArray["name"],
$nameTypeArray['type']
);
}
}

$myStringLiteralConsumerClass = new MyStringLiteralConsumerClass('random name');
$myStringLiteralConsumerClass->displayNameAndType();

At first look all seems nice and easy (because it is in here) but…

Let’s imagine scenario where we realise that for some reason we need to update name key inside of
MyStringLiteralExposeClass->getArrayWithNameAndType function to be now specificName .

If you use IDE’s search function, only in that file you get 16(!) hits for searching name .
Yes, you can search for 'name' and you get (watch out!) only 1 hit.
Then, another option in PHP — you can search for "name" and you get also 1 hit.

Now imagine that these classes are in separated files and there are 50 different classes containing usage of key name in them but not all come from the same function, so not all will be the subject to update.
Now, I feel sorry for anyone who needs to find it, check context to define if that is what you need and won’t make typo while changing (yes, that is another common issue).

How can constant help in that case?

Let’s have a look at updated code example and analyse it:

<?php

namespace App;

class MyStringLiteralExposeClass {

// Yes, that needs to be public to be accessible.
public const PARAM_NAME = 'name';
public const PARAM_TYPE = 'type';

public function getArrayWithNameAndType(): array
{
return [
self::PARAM_NAME => 'Cool Name',
self::PARAM_TYPE => 'CoolType'
];
}

}

class MyStringLiteralConsumerClass {

public function __construct(
protected string $name,
protected MyStringLiteralExposeClass $myStringLiteralExposeClass = new MyStringLiteralExposeClass()
) {}

public function displayNameAndType(): void
{
$nameTypeArray = $this->myStringLiteralExposeClass->getArrayWithNameAndType();
echo sprintf(
'The name provided in construct is %s, and extracted name %s which has type %s',
$this->name,
$nameTypeArray[MyStringLiteralExposeClass::PARAM_NAME],
$nameTypeArray[MyStringLiteralExposeClass::PARAM_TYPE]
);
}
}

$myStringLiteralConsumerClass = new MyStringLiteralConsumerClass('random name');
$myStringLiteralConsumerClass->displayNameAndType();

What happens in here?

  1. We define public const PARAM_NAME inside of our MyStringLiteralExposeClass , so it is accessible by using :: in other classes.
  2. We use that constant in our getArrayWithNameAndType to represent key.
  3. Inside of MyStringLiteralConsumerClass , after getting array, we use MyStringLiteralExposeClass::PARAM_NAME as a key to extract value from associative array.

Let’s again imagine scenario where we realise that for some reason we need to update name key inside of
MyStringLiteralExposeClass->getArrayWithNameAndType function to be now specificName but with updated code.

Just use your IDE to find usage of PARAM_NAME constant and enjoy list of all placements where that is being used without worrying about context.

In that case, our life got easier for 3 scenarios:

  1. When we only need to change value for that constant — we just do it in one place.
  2. If for some reason it makes sense to change name of that constant along with its value — again, you got all occurrences of its usage.
  3. We do not need to worry about typos which can easily happen while using string literals (even if happened, we update one place).

How about some more real life example?

I am going to use Laravel as that is currently framework I am using on daily basis (although still thinking Symfony is great) and actually I find concept of using constants exceptionally useful for not very small projects.

I am going to cover 2 use-cases:

  1. Using constants for representing column names thorough the project
  2. Using constants for representing arrays/collection keys thorough the project

Let’s dive into using constants for column names

First, we will define model in which we use our constants.

<?php

namespace App\Models;

class User
{
public const TABLE_NAME = 'users';

public const COLUMN_ID = 'id';
public const COLUMN_NAME = 'name';
public const COLUMN_EMAIL = 'email';
public const COLUMN_PASSWORD = 'password';

// Updated version with constants
protected $fillable = [
self::COLUMN_NAME,
self::COLUMN_EMAIL,
self::COLUMN_PASSWORD,
];

// Version with string literals (usual one)
protected $fillable = [
'name', 'email', 'password'
]

// Updated version with constants
protected $hidden = [
self::COLUMN_PASSWORD,
];

// Version with string literals (usual one)
protected $hidden = [
'pasword' // <- SEE THE TYPO? - now the password will be exposed until fixed.
]
}

As you can see, I am using quite descriptive naming for constants:

  • TABLE_NAME — representing (surprise!) table name
  • COLUMN_{columnName} — representing (again, surprise!) column name.

These constants are being used instead of string literals already in the Model but that is just a beginning. So, let’s look at migrations:

<?php

use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
/**
* UPDATED VERSION WITH CONSTANTS - all have source in one place.
* Have you deleted some constant in Model but forgot to delete from here? You will get expected error
*/
Schema::create(User::TABLE_NAME, function (Blueprint $table) {
$table->id();
$table->string(User::COLUMN_NAME);
$table->string(User::COLUMN_EMAIL)->unique();
$table->string(User::COLUMN_PASSWORD);
$table->timestamps();
});

/**
* Usual version with string literals where typos are just beginning.
* Have you forgotten to delete some column in DB in here? Let it hang until it is somehow discovered
*/
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->timestamps();
});
}

public function down(): void
{
Schema::dropIfExists(User::TABLE_NAME);

// imagine typo here and deleting different table...
Schema::dropIfExists('users');
}
};

As we can see, we can easily use constants defined inside Model also here.
Do you need to change Table Name or some Column Name? Just do it in the model and that is it.

But, it would not be beneficial if that was all… Let’s explore how it can be useful in our Request Validation:

<?php

namespace App\Http\Requests\User;

use App\Models\User;

class CreateUserRequest extends FormRequest
{
public function rules(): array
{
$allParams = $this->all();

/**
* one of scenarios is we want body request to contain JSON with column names as keys.
* In that case, using constant make it much easier becase again, you control everything from one place.
*/
return [
User::COLUMN_NAME => [
'required',
'string',
'min:2'
],
User::COLUMN_EMAIL => [
'required',
'email',
Rule::unique(
User::TABLE_NAME,
User::COLUMN_EMAIL
)->ignore($allParams[User::COLUMN_ID]), // also used as array key
],
User::COLUMN_PASSWORD => [
'required',
'string',
'confirmed',
'min:16', // must be at least 16 characters in length
'regex:/[a-z]/', // must contain at least one lowercase letter
'regex:/[A-Z]/', // must contain at least one uppercase letter
'regex:/[0-9]/', // must contain at least one digit
'regex:/[@$!%*#?&-_]/', // must contain a special character
],
];
}
}

As visible in the example code, we expect our body request to contain payload with column names used as keys. Inside of Request Class , we can simply use our Model’s constants and reduce our list of worries to minimum.

But, to see it being even more powerful in big repos, let’s explore another usage: this time we will have UserQuery Class which will be responsible for building data delivered to query database (not covering in this article usage of it but at some point I will write one).

<?php

namespace App\Tools\ValueObjects\Queries\User;

class CreateUserQuery
{
public function __construct(
private readonly string $name,
private readonly string $email,
private readonly string $password
) {}

public function getName(): string
{
return $this->name;
}

public function getEmail(): string
{
return $this->email;
}

public function getHashedPassword(): string
{
return Hash::make( $this->password );
}

/**
* That function is responsible for creating array of columns which we want to use to create Entity
* And assign values to them.
* It makes sure we don't update unwanted columns.
*
* @return array
*/
public function getFillableData(): array
{
return array_filter([
User::COLUMN_NAME => $this->getName(),
User::COLUMN_EMAIL => $this->getEmail(),
User::COLUMN_PASSWORD => $this->getHashedPassword(),
]);
}
}

How is that significant? Our getFillableData method returns array with columns used as keys and values assigned to them. That way, we don’t need to worry about typos because constants are immutable.

Since real benefit of using constants instead of string literals grows along with code base, we can use that concept also in Factory in Laravel:

<?php

namespace Database\Factories;

class UserFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
User::COLUMN_NAME => fake()->name(),
User::COLUMN_EMAIL => fake()->safeEmail(),
User::COLUMN_PASSWORD => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
];
}
}

Again, we operate on constants. Changing value inside of the model will cover every single usage of it, so we don’t need to waste time on playing detective.

As all developers using Laravel know, when dealing with models, you can access Model attributes (columns) directly and in this case, constants will require a bit of additional work (please, ignore breaking SRP and not dealing with Exception but I want to focus on matter):

<?php

class MyModelConstantClass {

/**
* Here we just fetch model (could use findOrFail() method and handle Exception...
* And we use the easiest way offered by Laravel to fetch user's name
*/
public function usualUsageOfModelAttributes(): void
{
$user = User::find(1);
echo sprintf(
'User name is: %s',
$user->name // in that case, using constants is no-go
);
}

/**
* Here is example of retrieving User's name with using constant
* As presented, constant COLUMN_NAME is defined in User Model
*/
public function usageOfConstantToGetModelAttributes(): void
{
$user = User::find(1);
echo sprintf(
'User name is: %s',
$user->getAttribute(User::COLUMN_NAME) // in general, I find using getAttribute() much better...
)
}
}

In that case, the easy way is replaced with slightly more complicated one and at first it does not seem like it can bring any profit but, based on my own experience, I am happy when somebody decided to put extra effort and I can locate needed attribute without pulling my hair out.

These were only a few cases where some of the ‘string’ would be repeated throughout the whole codebase. Imagine more of these to be used in Services, Controllers, Tests etc… List grows but by using constants you can easily find every single occurrence of constant with IDE.

I think that it is enough get get grasp of the concept, so…

Let’s now look into Using constants for representing arrays/collection keys thorough the project

That is another, very basic example of how we can use constants but I want to show that this can also make code look quite uglier (kind of like in example with fetching Model’s attributes), so not everything is just perfect, especially when it comes to the length of lines.

<?php

namespace Database\Factories;

/**
* Let's not worry about Exception in here...
*/
class MyConstantExporterClass
{
public const KEY_FIRST_DATA_PROVIDER_RESPONSE = 'firstDataProviderResponse';
public const KEY_SECOND_DATA_PROVIDER_RESPONSE = 'secondDataProviderResponse';

public function gatherResponses(): array {
// ... here I have code to gather responses...

/**
* Yes, we could use Value Object or DTO here which would make sense,
* but for that example we will keep it simple
*/
return [
// let's pretend the values are defined... :)
self::KEY_SECOND_DATA_PROVIDER_RESPONSE => $firstResponse,
self::KEY_SECOND_DATA_PROVIDER_RESPONSE => $secondResponse
];
}
}

class MyConstantImporterClass
{
public function __construct(
protected MyConstantExporterClass $constantExporterClass = new MyConstantExporterClass()
) {}

public function handleResponses()
{
$responses = $this->constantExporterClass->gatherResponses();

$this
->transformFirstDataProviderResponse(
$responses[MyConstantExporterClass::KEY_FIRST_DATA_PROVIDER_RESPONSE]
)
->transformSecondDataProviderResponse(
$responses[MyConstantExporterClass::KEY_SECOND_DATA_PROVIDER_RESPONSE]
);

return $responses;
}

protected function transformFirstDataProviderResponse( array &$response ): self
{
// some transformation happens...

return $this;
}

protected function transformSecondDataProviderResponse( array &$response ): self
{
// some transformation happens...

return $this;
}
}

$myConstantImporterClass = new MyConstantImporterClass;
$responses = $myConstantImporterClass->handleResponses();

dd(
$responses[MyConstantExporterClass::KEY_FIRST_DATA_PROVIDER_RESPONSE],
$responses[MyConstantExporterClass::KEY_SECOND_DATA_PROVIDER_RESPONSE]
);

As visible — sometimes constants can get really long name if we want to keep it descriptive (although we don’t have to…?). It is up to the developer to decide if sometimes it is worth to sacrifice visibility (although it is not the worst it can be) for usability.

One way to deal with that is reducing name of class we are dealing with by using alias.

Summary: Why Constants Shine Over String Literals

When coding in PHP, using constants over string literals brings big benefits:

  • Clearer Code: Constants like MyModel::COLUMN_NAME instantly tell us more than just a plain 'name'. We know it's tied to MyModel, so it's easier to understand what's going on.
  • Easy Updates: Need to change a name or value? With constants, change it in one place and you’re done. No risky global searches or missed spots.
  • Fewer Mistakes: It’s easy to mistype a string. But with constants, your coding tool will likely catch the mistake for you.
  • Quick Searches: Using tools like PhpStorm, you can find where a constant is used in seconds. It’s a real time-saver.
  • Built-in Guide: Constants also act like mini-guides, helping others get what you were aiming for in your code.

In short, while both have their uses, constants offer a clearer, safer, and more efficient way to code in PHP.

Because why to sacrifice a few minutes now when we can spend a few hours debugging later…

Have you found this article helpful and want to keep me awake for writing more?
Here is your chance to get me a coffee: https://www.buymeacoffee.com/grzegorzlaT

Greetings,
Grzegorz

--

--

Grzegorz Lasak (Gregster)

Mastering the art of PHP, JS and GO on daily basis while working as a Web Developer. Still trying to master the art of adulting.