Opaque constants as symbols?

Something I like in Ruby is symbols.

Sometimes the values of our constants in PHP don’t matter because they are only ever used in comparisons and array keys. For example, the app might have options like these

const FORMAT_CSV = 'csv';
const FORMAT_EXCEL = 'excel';
const FORMAT_SQLITE = 'sqlite';

The values could have been 1, 2, and 3 or 'x', 'y', and 'z' and the app would work the same.

Now imagine that the app allows the user to select from a number of duration intervals and one is declared as

const INTERVAL_1_WEEK = '1week'; 

A coder might by mistake use it, e.g. in DateTime::modify(). It would be a programming error to do that but you can imagine how it might happen. And it could lead to a nasty bug that’s hard to understand. In our team we had the habit of assigning meaningful string values to such constants and we ran into a nasty bug of exactly this kind.

Once we figured it out I saw that the programming error would have been avoided or detected early if we had used something like a Ruby symbol. PHP doesn’t have them so I wondered if we might approximate them with opaque values, e.g.

const FORMAT_CSV = 'rtPL7NrvWN';
const FORMAT_EXCEL = 'u672AdVSkW';
const FORMAT_SQLITE = 'bw6bs6LbED';

The chance of these values meaning something in any context is remote. It seems to me this forces us to write more robust, less-coupled code.

What do you think of the idea? Do any of you do that?

Debugging such values with XDebug will be hard. You’ll have to look up the meaning.

Likely value objects would do:

class Format
{
    private string $format;

    private function __construct()
    {
    }

     public static function csv(): self
     {
         $new = new self();
         $new->format = 'csv';
         return $new;
     }

     // same for other formats
}

class Converter
{
    public function convert(Format $format)
    {
        // ...
    }
}

As an alternative, you can leave constructor public and validate value there.

As another alternative, use constants as usual but validate value where it’s used.

Fair point. And when browsing a database directly it’s useful to be able to understand enum values.

Maybe a compromise…

const FORMAT_CSV = 'csv_rtPL7N';
const FORMAT_EXCEL = 'excel_u672Ad';
const FORMAT_SQLITE = 'sqlite_bw6bs6';

Not opaque but still likely to error if passed by mistake.

The pros/cons of all these alternatives are rather unconvincing.

So how about validating values?

class TheForm extends \yii\base\Model
{
    const FORMAT_CSV = 'csv_rtPL7N';
    const FORMAT_EXCEL = 'excel_u672Ad';
    const FORMAT_SQLITE = 'sqlite_bw6bs6';

    public $reportFormat;

    public function rules()
    {
        return [
            ['reportFormat', 'in', 'range' => array_keys(self::formatsList())],
        ];
    }

    // also used to build the drop-down or whatever
    public static function formatsList()
    {
        return [
            self::FORMAT_CSV => Yii::t('models/forms', 'Comma Separated Values (CSV)'),
            self::FORMAT_EXCEL => Yii::t('models/forms', 'Microsoft Excel (XLSX)'),
            self::FORMAT_SQLITE => Yii::t('models/forms', 'Sqlite (.db file)'),
        ];
    }
}

Since it now validates, does it still make sense to add these hashes?

They are just random strings, not hashes.

They prevent inappropriate use of the string values. If we had

const FORMAT_CSV = 'csv',

then it would be easy to make this programming mistake

$filename = 'Export-Data.' . FORMAT_CSV;

Alright. I don’t think that happens often enough though.