├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── composer.json ├── global ├── autoloader.php ├── bool-array.php ├── double-array.php ├── float-array.php ├── functions.php ├── int-array.php └── string-array.php ├── phpunit.xml ├── psalm.xml ├── src ├── AbstractTypedArray.php ├── BoolArray.php ├── FloatArray.php ├── IntArray.php ├── ObjectTypedArray.php └── StringArray.php └── tests └── BasicTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | phpunit: 7 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} 8 | runs-on: ${{ matrix.operating-system }} 9 | strategy: 10 | matrix: 11 | operating-system: ['ubuntu-latest'] 12 | php-versions: ['8.3', '8.4'] 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup PHP 19 | uses: shivammathur/setup-php@v2 20 | with: 21 | php-version: ${{ matrix.php-versions }} 22 | extensions: mbstring, intl, sodium 23 | ini-values: error_reporting=-1, display_errors=On 24 | coverage: none 25 | 26 | - name: Install Composer dependencies 27 | uses: "ramsey/composer-install@v2" 28 | 29 | - name: PHPUnit tests 30 | run: vendor/bin/phpunit 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /vendor 3 | /composer.lock 4 | /composer.phar 5 | /.phpunit.cache 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Strictly Typed Arrays in PHP 8 2 | 3 | [![Build Status](https://github.com/paragonie/typed-arrays/actions/workflows/ci.yml/badge.svg)](https://github.com/paragonie/typed-arrays/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/paragonie/typed-arrays/v/stable)](https://packagist.org/packages/paragonie/typed-arrays) 5 | [![Latest Unstable Version](https://poser.pugx.org/paragonie/typed-arrays/v/unstable)](https://packagist.org/packages/paragonie/typed-arrays) 6 | [![License](https://poser.pugx.org/paragonie/typed-arrays/license)](https://packagist.org/packages/paragonie/typed-arrays) 7 | [![Downloads](https://img.shields.io/packagist/dt/paragonie/typed-arrays.svg)](https://packagist.org/packages/paragonie/typed-arrays) 8 | 9 | **Requires PHP 8.3**. This is best described through example: 10 | 11 | ```php 12 | foo, $x->bar); 28 | var_dump($x->foo[1]); 29 | ``` 30 | 31 | This should output the following: 32 | 33 | ``` 34 | object(string⟦⟧)#5 (2) { 35 | [0]=> 36 | string(5) "apple" 37 | [1]=> 38 | string(3) "bee" 39 | } 40 | object(int⟦⟧)#6 (3) { 41 | [0]=> 42 | int(4) 43 | [1]=> 44 | int(5) 45 | [2]=> 46 | int(120000) 47 | } 48 | string(3) "bee" 49 | ``` 50 | 51 | If you try to pass an incorrect type, you'll get a `TypeError`: 52 | 53 | ```php 54 | foo, $x->bar); 69 | ``` 70 | 71 | Should produce: 72 | 73 | ```terminal 74 | Fatal error: Uncaught TypeError: string⟦⟧(): Argument #3 must be of type string, int given 75 | ``` 76 | 77 | ## What Is This Package Doing? 78 | 79 | We are using Unicode characters (`⟦` and `⟧`) to create a class that implements `ArrayAccess`. 80 | All arguments to these types are then strictly typed. 81 | 82 | In effect, we have turned a class into a typed array that your IDE will not complain about. 83 | 84 | ## Does It Support Multi-Level Types? e.g. `string⟦⟧⟦⟧` 85 | 86 | You betcha. 87 | 88 | ```php 89 | double); 105 | ``` 106 | 107 | This will produce: 108 | 109 | ```terminal 110 | object(string⟦⟧⟦⟧)#7 (2) { 111 | [0]=> 112 | object(string⟦⟧)#5 (1) { 113 | [0]=> 114 | string(4) "test" 115 | } 116 | [1]=> 117 | object(string⟦⟧)#6 (1) { 118 | [0]=> 119 | string(7) "example" 120 | } 121 | } 122 | ``` 123 | 124 | ## Does This Support Arrays of Classes? 125 | 126 | Of course! 127 | 128 | ```php 129 | 151 | object(Foo⟦⟧)#5 (1) { 152 | [0]=> 153 | object(Foo)#6 (0) { 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | ### How Does This Create Types for My Classes? 160 | 161 | See: [the autoloader](global/autoloader.php). 162 | 163 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "paragonie/typed-arrays", 3 | "description": "Strictly typed scalar arrays for PHP 8.3 and newer", 4 | "keywords": [ 5 | "typed arrays", 6 | "strict typing", 7 | "scalar types" 8 | ], 9 | "license": "ISC", 10 | "authors": [ 11 | { 12 | "name": "Paragon Initiative Enterprises, LLC", 13 | "email": "security@paragonie.com", 14 | "homepage": "https://paragonie.com" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "ParagonIE\\TypedArrays\\": "src" 20 | }, 21 | "files": ["global/autoloader.php"] 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "ParagonIE\\TypedArrays\\Tests\\": "tests" 26 | } 27 | }, 28 | "require": { 29 | "php": "^8.3" 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^9", 33 | "vimeo/psalm": "^4" 34 | } 35 | } -------------------------------------------------------------------------------- /global/autoloader.php: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 23 | 24 | src 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/AbstractTypedArray.php: -------------------------------------------------------------------------------- 1 | contents; 14 | } 15 | 16 | #[\Override] 17 | public function offsetExists(mixed $offset): bool 18 | { 19 | return array_key_exists($offset, $this->contents); 20 | } 21 | 22 | #[\Override] 23 | public function offsetGet(mixed $offset): mixed 24 | { 25 | if (!$this->offsetExists($offset)) { 26 | throw new \RangeException('Index not found: ' . $offset); 27 | } 28 | return $this->contents[$offset]; 29 | } 30 | 31 | #[\Override] 32 | public function offsetSet(mixed $offset, mixed $value): void 33 | { 34 | switch (static::SCALAR_TYPE) { 35 | case 'mixed': 36 | break; 37 | case 'string': 38 | if (!is_string($value)) { 39 | throw new \TypeError('Only ' . static::SCALAR_TYPE . ' types can be assigned'); 40 | } 41 | break; 42 | case 'int': 43 | if (!is_int($value)) { 44 | throw new \TypeError('Only ' . static::SCALAR_TYPE . ' types can be assigned'); 45 | } 46 | break; 47 | case 'float': 48 | if (!is_float($value) && !is_int($value)) { 49 | throw new \TypeError('Only ' . static::SCALAR_TYPE . ' types can be assigned'); 50 | } 51 | break; 52 | case 'bool': 53 | if (!is_bool($value)) { 54 | throw new \TypeError('Only ' . static::SCALAR_TYPE . ' types can be assigned'); 55 | } 56 | break; 57 | case 'object': 58 | if (!is_object($value)) { 59 | throw new \TypeError('Only ' . static::SCALAR_TYPE . ' types can be assigned'); 60 | } 61 | break; 62 | } 63 | $this->contents[$offset] = $value; 64 | } 65 | 66 | #[\Override] 67 | public function offsetUnset(mixed $offset): void 68 | { 69 | if (array_key_exists($offset, $this->contents)) { 70 | unset($this->contents[$offset]); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/BoolArray.php: -------------------------------------------------------------------------------- 1 | contents = $arguments; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/FloatArray.php: -------------------------------------------------------------------------------- 1 | contents = $arguments; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/IntArray.php: -------------------------------------------------------------------------------- 1 | contents = $arguments; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ObjectTypedArray.php: -------------------------------------------------------------------------------- 1 | $argument) { 13 | if (!is_object($argument)) { 14 | throw new \TypeError("Argument at index {$index} is not an object"); 15 | } 16 | $type = static::OBJECT_TYPE; 17 | if (!$argument instanceof $type) { 18 | throw new \TypeError("Argument at index {$index} is the wrong type"); 19 | } 20 | } 21 | $this->contents = $arguments; 22 | } 23 | 24 | #[\Override] 25 | public function offsetSet(mixed $offset, mixed $value): void 26 | { 27 | $type = static::OBJECT_TYPE; 28 | if (!$value instanceof $type) { 29 | throw new \TypeError("Value is the wrong type"); 30 | } 31 | $this->contents[$offset] = $value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/StringArray.php: -------------------------------------------------------------------------------- 1 | contents = $arguments; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/BasicTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(BoolArray::class, $bools); 28 | $this->assertInstanceOf(FloatArray::class, $floats); 29 | $this->assertInstanceOf(IntArray::class, $ints); 30 | $this->assertInstanceOf(StringArray::class, $strings); 31 | 32 | $this->assertIsBool($bools[0]); 33 | $this->assertIsFloat($floats[0]); 34 | $this->assertIsInt($ints[0]); 35 | $this->assertIsString($strings[0]); 36 | } 37 | } --------------------------------------------------------------------------------