├── .editorconfig ├── .github └── workflows │ └── run-tests.yml ├── LICENCE ├── Makefile ├── README.md ├── composer.json ├── renovate.json └── src ├── Exception ├── InvalidFormatException.php ├── InvalidKeyException.php ├── InvalidValueException.php └── MagicConstantException.php └── MagicConstant.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | end_of_line = LF 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | 18 | [*.yml] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | php: [ 8.2, 8.3, 8.4 ] 12 | dependency-version: [ prefer-lowest, prefer-stable ] 13 | 14 | name: P${{ matrix.php }} - ${{ matrix.dependency-version }} 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php }} 24 | coverage: none 25 | 26 | - name: Cache dependencies 27 | uses: actions/cache@v4 28 | with: 29 | path: ~/.composer/cache/files 30 | key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 31 | 32 | - name: Install dependencies 33 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 34 | 35 | - name: Run PHPStan 36 | run: php vendor/bin/phpstan analyse --no-progress --no-interaction --no-ansi --memory-limit=-1 37 | 38 | - name: Execute tests 39 | run: php vendor/bin/phpunit 40 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 CuyZ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := help 2 | 3 | .PHONY: help 4 | help: ## Show help message 5 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 6 | 7 | .PHONY: install 8 | install: ## to setup the dev environment. 9 | composer install 10 | 11 | .PHONY: test 12 | test: ## to perform unit tests. 13 | php vendor/bin/phpunit 14 | 15 | .PHONY: coverage 16 | coverage: ## to perform unit tests with code coverage. 17 | php -d xdebug.mode=coverage vendor/bin/phpunit --coverage-text 18 | 19 | .PHONY: phpstan 20 | phpstan: ## to run PHPStan 21 | php vendor/bin/phpstan analyse 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magic Constant 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | [![Total Downloads][ico-downloads]][link-downloads] 5 | [![Software License][ico-license]](LICENSE) 6 | 7 | This library allows you to create enum-like classes that support multiple 8 | formats for each key. 9 | 10 | It helps represent [magic numbers and strings][link-wikipedia] in code. 11 | 12 | ## Example 13 | 14 | Let's say your code has to interact with two services about some contracts. 15 | 16 | To represent an active contract: 17 | 18 | - Service A uses `active` 19 | - Service B uses `10` 20 | 21 | Using a magic constant you declare the following class: 22 | 23 | ```php 24 | use CuyZ\MagicConstant\MagicConstant; 25 | 26 | class ContractStatus extends MagicConstant 27 | { 28 | protected const ACTIVE = [ 29 | self::FORMAT_SERVICE_A => 'active', 30 | self::FORMAT_SERVICE_B => 10, 31 | ]; 32 | 33 | // Others status... 34 | 35 | public const FORMAT_SERVICE_A = 'a'; 36 | public const FORMAT_SERVICE_B = 'b'; 37 | } 38 | ``` 39 | 40 | You can then use it like this: 41 | 42 | ```php 43 | // Instead of doing this: 44 | if ($status === 'active' || $status === 10) { 45 | // 46 | } 47 | 48 | // You can do this: 49 | if ($status->equals(ContractStatus::ACTIVE())) { 50 | // 51 | } 52 | ``` 53 | 54 | ## Installation 55 | 56 | ```bash 57 | $ composer require cuyz/magic-constant 58 | ``` 59 | 60 | ## Usage 61 | 62 | ```php 63 | use CuyZ\MagicConstant\MagicConstant; 64 | 65 | /** 66 | * You can declare static methods to help with autocompletion: 67 | * 68 | * @method static Example FOO() 69 | * @method static Example BAR() 70 | * @method static Example FIZ() 71 | */ 72 | class Example extends MagicConstant 73 | { 74 | // Only protected constants are used as keys 75 | protected const FOO = 'foo'; 76 | 77 | // A key can have multiple possible formats for it's value 78 | protected const BAR = ['bar', 'BAR', 'b']; 79 | 80 | // You can use an associative array to declare formats 81 | protected const FIZ = [ 82 | self::FORMAT_LOWER => 'fiz', 83 | self::FORMAT_UPPER => 'FIZ', 84 | ]; 85 | 86 | // Using constants for formats is not mandatory 87 | public const FORMAT_LOWER = 'lower'; 88 | public const FORMAT_UPPER = 'upper'; 89 | } 90 | ``` 91 | 92 | You can then use the class everywhere: 93 | 94 | ```php 95 | // As a parameter typehint and/or a return typehint 96 | function hello(Example $example): Example { 97 | // 98 | } 99 | 100 | hello(new Example('foo')); 101 | 102 | // You can also use constants keys as a static method 103 | hello(Example::BAR()); 104 | ``` 105 | 106 | ## Methods 107 | 108 | #### Get an instance value 109 | 110 | ```php 111 | echo (new Example('foo'))->getValue(); // 'foo' 112 | 113 | // You can specify the desired output format 114 | echo (new Example('FIZ'))->getValue(Example::FORMAT_LOWER); // 'fiz' 115 | ``` 116 | 117 | #### Get an instance key 118 | 119 | ```php 120 | $constant = new Example('b'); 121 | 122 | echo $constant->getKey(); // 'BAR' 123 | ``` 124 | 125 | #### Get instances with all possible formats 126 | 127 | ```php 128 | $constant = new Example('fiz'); 129 | 130 | echo $constant->getAllFormats(); // [new Example('fiz'), new Example('FIZ')] 131 | ``` 132 | 133 | #### Get all possible values for an instance 134 | 135 | ```php 136 | $constant = new Example('BAR'); 137 | 138 | echo $constant->getAllValues(); // ['bar', 'BAR', 'b'] 139 | ``` 140 | 141 | #### Returns a new instance where the value is from the first format 142 | 143 | ```php 144 | $constant = new Example('BAR'); 145 | 146 | echo $constant->normalize(); // new Example('bar') 147 | ``` 148 | 149 | #### Compares instances 150 | 151 | ```php 152 | (new Example('foo'))->equals(new Exemple('bar')); // false 153 | (new Example('foo'))->equals(null); // false 154 | 155 | (new Example('fiz'))->equals(new Exemple('FIZ')); // true 156 | (new Example('b'))->equals(new Exemple('b')); // true 157 | ``` 158 | 159 | #### Returns true if at least one element is equal 160 | 161 | ```php 162 | $constant = new Example('foo'); 163 | 164 | $constant->in([new Exemple('bar'), null, 'foo']); // false 165 | $constant->in([new Exemple('foo'), null, 'foo']); // true 166 | ``` 167 | 168 | #### Get all keys for a magic constant class 169 | 170 | ```php 171 | Example::keys(); // ['FOO', 'BAR', 'FIZ'] 172 | ``` 173 | 174 | #### Get an associative array of possible values 175 | 176 | ```php 177 | Example::values(); 178 | 179 | [ 180 | 'FOO' => new Example('foo'), 181 | 'BAR' => new Example('bar'), 182 | 'FIZ' => new Example('fiz'), 183 | ]; 184 | 185 | // You can specify a regex pattern to match certain keys 186 | Example::values('/^F(.+)/'); 187 | 188 | [ 189 | 'FOO' => new Example('foo'), 190 | 'FIZ' => new Example('fiz'), 191 | ]; 192 | ``` 193 | 194 | #### Get all keys and associated values 195 | 196 | ```php 197 | Example::toArray(); 198 | 199 | [ 200 | 'FOO' => ['foo'], 201 | 'BAR' => ['bar', 'BAR', 'b'], 202 | 'FIZ' => ['fiz', 'FIZ'], 203 | ]; 204 | ``` 205 | 206 | #### Check if a value is valid 207 | 208 | ```php 209 | Example::isValidValue('foo'); // true 210 | Example::isValidValue('hello'); // false 211 | ``` 212 | 213 | #### Check if a key is valid 214 | 215 | ```php 216 | Example::isValidKey('BAR'); // true 217 | Example::isValidKey('HELLO'); // false 218 | ``` 219 | 220 | #### Returns the key of any value 221 | 222 | ```php 223 | Example::search('foo'); // 'FOO' 224 | Example::search('b'); // 'BAR' 225 | Example::search('hello'); // false 226 | ``` 227 | 228 | [ico-version]: https://img.shields.io/packagist/v/cuyz/magic-constant.svg 229 | [ico-downloads]: https://img.shields.io/packagist/dt/cuyz/magic-constant.svg 230 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 231 | 232 | [link-packagist]: https://packagist.org/packages/cuyz/magic-constant 233 | [link-downloads]: https://packagist.org/packages/cuyz/magic-constant 234 | [link-wikipedia]: https://en.wikipedia.org/wiki/Magic_number_(programming) 235 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cuyz/magic-constant", 3 | "description": "PHP Magic Constants, even more powerful than an Enum", 4 | "keywords": [ 5 | "enum", 6 | "magic constant", 7 | "magic number" 8 | ], 9 | "type": "library", 10 | "license": "MIT", 11 | "homepage": "https://github.com/CuyZ/MagicConstant", 12 | "authors": [ 13 | { 14 | "name": "Nathan Boiron", 15 | "email": "nathan.boiron@gmail.com", 16 | "homepage": "https://github.com/Mopolo", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "8.2.* || 8.3.* || 8.4.*" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "CuyZ\\MagicConstant\\": "src" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "CuyZ\\MagicConstant\\Tests\\": "tests" 31 | } 32 | }, 33 | "require-dev": { 34 | "phpunit/phpunit": "^11.0", 35 | "phpstan/phpstan": "2.1.16" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/Exception/InvalidFormatException.php: -------------------------------------------------------------------------------- 1 | , array> */ 19 | protected static array $cache = []; 20 | 21 | final public function __construct(mixed $value) 22 | { 23 | if ($value instanceof self) { 24 | $value = $value->getValue(); 25 | } 26 | 27 | $this->setValue($value); 28 | } 29 | 30 | public function getValue(string|int|null $format = null): mixed 31 | { 32 | if (empty($format)) { 33 | return $this->value; 34 | } 35 | 36 | $values = self::toArray(); 37 | 38 | if (!isset($values[$this->getKey()][$format])) { 39 | throw new InvalidFormatException($this, $format); 40 | } 41 | 42 | return $values[$this->getKey()][$format]; 43 | } 44 | 45 | /** 46 | * @return static[] 47 | */ 48 | public function getAllFormats(): array 49 | { 50 | $values = self::toArray(); 51 | $instances = array_map( 52 | function ($value) { 53 | return new static($value); 54 | }, 55 | $values[$this->getKey()] 56 | ); 57 | 58 | return array_values($instances); 59 | } 60 | 61 | /** 62 | * @return mixed[] 63 | */ 64 | public function getAllValues(): array 65 | { 66 | $values = self::toArray(); 67 | 68 | return array_values($values[$this->getKey()]); 69 | } 70 | 71 | public function getKey(): string 72 | { 73 | return (string)self::search($this->value); 74 | } 75 | 76 | /** 77 | * Returns the current instance format. 78 | */ 79 | public function getFormat(): int|string|null 80 | { 81 | $values = self::toArray(); 82 | 83 | foreach ($values[$this->getKey()] as $format => $value) { 84 | if ($value === $this->value) { 85 | return $format; 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | 92 | public function __toString(): string 93 | { 94 | return (string)$this->value; 95 | } 96 | 97 | public function normalize(): MagicConstant 98 | { 99 | $array = self::toArray(); 100 | $key = $this->getKey(); 101 | 102 | $values = array_values($array[$key]); 103 | 104 | return new static($values[0]); 105 | } 106 | 107 | protected function setValue(mixed $value): void 108 | { 109 | if (!static::isValidValue($value)) { 110 | throw new InvalidValueException(static::class, $value); 111 | } 112 | 113 | $this->value = $value; 114 | } 115 | 116 | final public function equals(?MagicConstant $other): bool 117 | { 118 | if ($other === null) { 119 | return false; 120 | } 121 | 122 | if (get_called_class() !== get_class($other)) { 123 | return false; 124 | } 125 | 126 | $ownKey = $this->getKey(); 127 | $otherKey = self::search($other->getValue()); 128 | 129 | return $ownKey === $otherKey; 130 | } 131 | 132 | /** 133 | * @param mixed[] $values 134 | */ 135 | public function in(array $values): bool 136 | { 137 | foreach ($values as $value) { 138 | if (!($value instanceof static)) { 139 | continue; 140 | } 141 | 142 | if ($this->equals($value)) { 143 | return true; 144 | } 145 | } 146 | 147 | return false; 148 | } 149 | 150 | public function toFormat(string $format): static 151 | { 152 | return new static($this->getValue($format)); 153 | } 154 | 155 | /** 156 | * @return string[] 157 | */ 158 | public static function keys(): array 159 | { 160 | return array_keys(self::toArray()); 161 | } 162 | 163 | /** 164 | * @return static[] 165 | */ 166 | public static function values(?string $pattern = null): array 167 | { 168 | $out = []; 169 | 170 | foreach (self::toArray() as $key => $values) { 171 | if (null === $pattern || preg_match($pattern, $key)) { 172 | $out[$key] = new static(reset($values)); 173 | } 174 | } 175 | 176 | return $out; 177 | } 178 | 179 | private static function toArray(): array 180 | { 181 | if (!array_key_exists(static::class, static::$cache)) { 182 | $reflection = new ReflectionClass(static::class); 183 | $constants = $reflection->getReflectionConstants(); 184 | 185 | $cache = []; 186 | 187 | foreach ($constants as $constant) { 188 | if (!$constant->isProtected()) { 189 | continue; 190 | } 191 | 192 | $value = $constant->getValue(); 193 | 194 | if (!is_array($value)) { 195 | $value = [$value]; 196 | } 197 | 198 | $cache[$constant->name] = $value; 199 | } 200 | 201 | static::$cache[static::class] = $cache; 202 | } 203 | 204 | return static::$cache[static::class]; 205 | } 206 | 207 | public static function isValidValue(mixed $value): bool 208 | { 209 | return false !== self::search($value); 210 | } 211 | 212 | public static function isValidKey(mixed $key): bool 213 | { 214 | $array = self::toArray(); 215 | 216 | return isset($array[$key]); 217 | } 218 | 219 | private static function search(mixed $value): string|false 220 | { 221 | /** 222 | * @var string $constant 223 | * @var array $values 224 | */ 225 | foreach (self::toArray() as $constant => $values) { 226 | if (in_array($value, $values, true)) { 227 | return $constant; 228 | } 229 | } 230 | 231 | return false; 232 | } 233 | 234 | public static function tryFrom(mixed $value): ?self 235 | { 236 | try { 237 | return new static($value); 238 | } catch (MagicConstantException $e) { 239 | return null; 240 | } 241 | } 242 | 243 | /** 244 | * @param array $arguments 245 | * @throws InvalidKeyException 246 | */ 247 | public static function __callStatic(string $name, array $arguments = []): static 248 | { 249 | $array = self::toArray(); 250 | 251 | if (!isset($array[$name])) { 252 | throw new InvalidKeyException(static::class, $name); 253 | } 254 | 255 | return new static(reset($array[$name])); 256 | } 257 | } 258 | --------------------------------------------------------------------------------