├── .gitignore ├── README.md ├── composer.json ├── example.php └── src └── Struct └── Struct.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Structs for PHP 2 | =============== 3 | Structs for PHP7 inspired by golang 4 | 5 | Usage 6 | ----- 7 | ```php 8 | struct('User', [ 9 | 'name' => 'string', 10 | 'age' => 'int', 11 | 'active' => 'bool', 12 | ]); 13 | 14 | $user = new User(); 15 | 16 | $user['name'] = 'Andy'; 17 | $user['age'] = 13; 18 | $user['active'] = true; 19 | 20 | $user['email'] = 'andybaird@gmail.com'; 21 | 22 | // Fatal error: Uncaught InvalidArgumentException: Struct does not contain property `email` 23 | 24 | $user['age'] = '22'; 25 | 26 | // Fatal error: Uncaught TypeException: Argument 1 passed to User::set_age() must be of the type integer, string given 27 | ``` 28 | 29 | Turn off strict type checking and allow variables to be coerced into types by simply calling: 30 | 31 | ```php 32 | Struct\Struct::$strict = false; 33 | $user['age'] = '22'; 34 | var_dump($user['age']); 35 | 36 | // int(22) 37 | ``` 38 | 39 | Under the hood, structs are simply classes implementing ArrayAccess and Iterable generated at run time. They have generated getter and setters for all fields that allow them to do the type checking. 40 | 41 | Filling a struct from an array: 42 | ```php 43 | $row = $db->fetchArray('select * from user where id=1'); 44 | $user->fromArray($row); 45 | ``` 46 | 47 | You can extend structs further by giving them their own methods. 48 | 49 | ```php 50 | struct('User', [ 51 | 'firstName' => 'string', 52 | 'lastName' => 'string', 53 | 'active' => 'bool', 54 | 'age' => 'int' 55 | ],[ 56 | 'fullName' => function() { 57 | return $this['firstName'] . ' ' . $this['lastName']; 58 | } 59 | ]); 60 | 61 | $user['firstName'] = 'Andy'; 62 | $user['lastName'] = 'Baird'; 63 | 64 | echo $user->fullName(); 65 | // Andy Baird 66 | ``` 67 | 68 | Add magic methods simply: 69 | ```php 70 | struct('User', [ 71 | 'firstName' => 'string', 72 | 'lastName' => 'string', 73 | 'active' => 'bool', 74 | 'age' => 'int' 75 | ],[ 76 | '__toString' => function() { 77 | return $this['firstName'] . ' ' . $this['lastName'] . ' is a ' . $this['age'] . ' year old ' . ($this['active'] ? 'active' : 'inactive') . ' user'; 78 | } 79 | ]); 80 | 81 | echo $user; 82 | // Andy Baird is a 13 year old inactive user 83 | ``` 84 | 85 | But... why? 86 | ----------- 87 | Just for my own experimentation. I would love to see structs implemented as a core feature of PHP, as I can see them being very appropriate for a more procedural or functional style of programming. 88 | 89 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajbdev/php-struct", 3 | "description": "Structs for PHP", 4 | "type": "library", 5 | "keywords": [ 6 | "struct" 7 | ], 8 | "license": "BSD-2-Clause", 9 | "authors": [ 10 | { 11 | "name": "Andy Baird", 12 | "email": "andybaird@gmail.com", 13 | "role": "lead" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=7" 18 | }, 19 | "autoload": { 20 | "files": ["src/Struct/Struct.php"] 21 | } 22 | } -------------------------------------------------------------------------------- /example.php: -------------------------------------------------------------------------------- 1 | 'string', 9 | 'age' => 'int', 10 | 'active' => 'bool', 11 | ]); 12 | 13 | $user = new User(); 14 | $user->fromArray(array( 15 | 'name' => 'George', 16 | 'age' => 36, 17 | 'extradata' => 'thisthat', 18 | 'active' => false, 19 | )); 20 | 21 | struct('User', [ 22 | 'firstName' => 'string', 23 | 'lastName' => 'string', 24 | 'active' => 'bool', 25 | 'age' => 'int' 26 | ],[ 27 | 'fullName' => function() { 28 | return $this['firstName'] . ' ' . $this['lastName']; 29 | }, 30 | 'birthYear' => function() { 31 | $year = date('Y') - $this['age']; 32 | 33 | return $year; 34 | }, 35 | '__toString' => function() { 36 | return $this->fullName() . ' is a ' . $this['age'] . ' year old ' . ($this['active'] ? 'active' : 'inactive') . ' user'; 37 | } 38 | ]); 39 | 40 | $user = new User(); 41 | 42 | 43 | 44 | 45 | echo $user; -------------------------------------------------------------------------------- /src/Struct/Struct.php: -------------------------------------------------------------------------------- 1 | name = $name; 51 | $this->properties = $properties; 52 | $this->methods = $methods; 53 | $this->src = ''; 54 | 55 | if (!preg_match('/^[A-Z]\w+/', $name)) { 56 | throw new \InvalidArgumentException('Invalid struct name: ' . $name); 57 | } 58 | 59 | $this->compile(); 60 | 61 | eval($this->src); 62 | 63 | return new $name(); 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getSource() { 70 | return $this->src; 71 | } 72 | 73 | /** 74 | * @param $property 75 | * @param $type 76 | */ 77 | public function addProperty($property, $type) { 78 | $this->properties[$property] = $type; 79 | } 80 | 81 | /** 82 | * Compile properties and methods into a string that resembles a PHP class. 83 | * 84 | * @return void 85 | */ 86 | protected function compile() { 87 | $this->classHeader(); 88 | $this->arrayHelpers(); 89 | $this->properties(); 90 | $this->methods(); 91 | $this->classFooter(); 92 | } 93 | 94 | /** 95 | * Provide array hydrate/export functionality 96 | * 97 | * @return void 98 | */ 99 | protected function arrayHelpers() { 100 | $this->src .= << \$val) { 105 | \$array[\$prop] = \$val; 106 | } 107 | return \$array; 108 | } 109 | TOARRAYHELPER; 110 | 111 | $existCheck = 'if (!$this->offsetExists($prop)) {'; 112 | if (self::$strict) { 113 | $existCheck .= 'throw new \InvalidArgumentException(\'Struct does not contain property `\' . $key . \'`\');'; 114 | } else { 115 | $existCheck .= 'continue;'; 116 | } 117 | $existCheck .= '}'; 118 | $this->src .= << \$val) { 122 | {$existCheck} 123 | 124 | \$this->offsetSet(\$prop,\$val); 125 | } 126 | } 127 | FROMARRAYHELPER; 128 | } 129 | 130 | /** 131 | * Attach methods to PHP class 132 | * 133 | * @return void 134 | */ 135 | protected function methods() { 136 | foreach ($this->methods as $name => $fn) { 137 | 138 | $ref = new \ReflectionFunction($fn); 139 | $filename = $ref->getFileName(); 140 | $start_line = $ref->getStartLine(); 141 | $end_line = $ref->getEndLine()-1; 142 | $length = $end_line - $start_line; 143 | $source = file($filename); 144 | $body = implode("", array_slice($source, $start_line, $length)); 145 | 146 | $this->src .= <<properties as $property => $type) { 163 | if (!in_array($type, array('int','string','float','bool')) && !class_exists($type)) { 164 | throw new \InvalidArgumentException('Unknown property type for ' . $property . ': ' . $type); 165 | } 166 | if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $property)) { 167 | throw new \InvalidArgumentException('Invalid property name: ' . $property); 168 | } 169 | $propArray[] = "'{$property}'"; 170 | 171 | $this->property($property, $type); 172 | } 173 | $this->src .= PHP_EOL . ' private $properties = array(' . implode(',', $propArray) . ');'; 174 | } 175 | 176 | /** 177 | * Attach a property to PHP class source 178 | * 179 | * @param $name 180 | * @param $type 181 | */ 182 | protected function property($name, $type) { 183 | $this->src .= <<{$name} = \$val; 189 | } 190 | 191 | private function get_{$name}():{$type} { 192 | return \$this->{$name}; 193 | } 194 | PROPERTY; 195 | 196 | } 197 | 198 | /** 199 | * End PHP class 200 | * 201 | * @return void 202 | */ 203 | protected function classFooter() { 204 | $this->src .= PHP_EOL . '}'; 205 | } 206 | 207 | /** 208 | * Boilerplate header for PHP class 209 | * 210 | * @return void 211 | */ 212 | protected function classHeader() { 213 | $prepend = ''; 214 | if (self::$strict === true) { 215 | $prepend = 'declare(strict_types=1);' . PHP_EOL . PHP_EOL; 216 | } 217 | 218 | $this->src .= sprintf(' 219 | %sclass %s implements \ArrayAccess, \Iterator {', $prepend, $this->name) . PHP_EOL . PHP_EOL; 220 | $this->src .= <<idx = 0; 225 | } 226 | 227 | public function current() { 228 | \$getter = 'get_' . \$this->properties[\$this->idx]; 229 | return \$this->{\$getter}(); 230 | } 231 | 232 | public function key() { 233 | return \$this->properties[\$this->idx]; 234 | } 235 | public function next() { 236 | ++\$this->idx; 237 | } 238 | public function rewind() { 239 | \$this->idx = 0; 240 | } 241 | public function valid() { 242 | return isset(\$this->properties[\$this->idx]); 243 | } 244 | 245 | public function offsetSet(\$key, \$value) { 246 | if (!\$this->offsetExists(\$key)) { 247 | throw new \InvalidArgumentException('Struct does not contain property `' . \$key . '`'); 248 | } 249 | \$setter = 'set_' . \$key; 250 | 251 | \$this->{\$setter}(\$value); 252 | } 253 | 254 | public function offsetExists(\$key) { 255 | return property_exists(\$this, \$key); 256 | } 257 | 258 | public function offsetUnset(\$key) { 259 | if (!\$this->offsetExists(\$key)) { 260 | throw new \InvalidArgumentException('Struct does not contain property `' . \$key . '`'); 261 | } 262 | 263 | \$this->{\$key} = null; 264 | } 265 | 266 | public function offsetGet(\$key) { 267 | if (!\$this->offsetExists(\$key)) { 268 | throw new \InvalidArgumentException('Struct does not contain property `' . \$key . '`'); 269 | } 270 | 271 | \$getter = 'get_' . \$key; 272 | 273 | return \$this->{\$getter}(); 274 | } 275 | BOILERPLATE; 276 | 277 | } 278 | } 279 | } 280 | 281 | --------------------------------------------------------------------------------