├── .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 | [](https://github.com/paragonie/typed-arrays/actions)
4 | [](https://packagist.org/packages/paragonie/typed-arrays)
5 | [](https://packagist.org/packages/paragonie/typed-arrays)
6 | [](https://packagist.org/packages/paragonie/typed-arrays)
7 | [](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 | }
--------------------------------------------------------------------------------