├── .gitignore ├── Commands └── ClearCache.php ├── DataMapper ├── EntityTrait.php ├── ModelDefinitionCache.php ├── QueryBuilder.php ├── QueryBuilderInterface.php ├── RelationDef.php └── ResultBuilder.php ├── Examples ├── Controllers │ ├── Crud.php │ ├── Like.php │ ├── Related.php │ ├── SubQuery.php │ └── Where.php ├── Entities │ ├── Color.php │ ├── Role.php │ ├── User.php │ └── UserDetail.php ├── Models │ ├── ColorModel.php │ ├── RoleModel.php │ ├── UserDetailModel.php │ └── UserModel.php └── Setup.php ├── Extensions ├── Database │ ├── BaseBuilder.php │ └── MySQLi │ │ ├── Builder.php │ │ ├── Connection.php │ │ ├── Forge.php │ │ └── Result.php ├── Entity.php └── Model.php ├── Hooks └── PreController.php ├── Interfaces └── OrmEventsInterface.php ├── LICENSE ├── Migration ├── ColumnTypes.php └── Table.php ├── ModelParser ├── ModelItem.php ├── ModelParser.php ├── PropertyItem.php ├── TypeScript │ ├── Index.php │ ├── Model.php │ ├── ModelDefinition.php │ └── ModelInterface.php └── Xamarin │ ├── Model.php │ └── ModelDefinition.php ├── README.md ├── composer.json └── debug_helper.php /.gitignore: -------------------------------------------------------------------------------- 1 | #------------------------- 2 | # Operating Specific Junk Files 3 | #------------------------- 4 | 5 | # OS X 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # OS X Thumbnails 11 | ._* 12 | 13 | # Windows image file caches 14 | Thumbs.db 15 | ehthumbs.db 16 | Desktop.ini 17 | 18 | # Recycle Bin used on file shares 19 | $RECYCLE.BIN/ 20 | 21 | # Windows Installer files 22 | *.cab 23 | *.msi 24 | *.msm 25 | *.msp 26 | 27 | # Windows shortcuts 28 | *.lnk 29 | 30 | # Linux 31 | *~ 32 | 33 | # KDE directory preferences 34 | .directory 35 | 36 | # Linux trash folder which might appear on any partition or disk 37 | .Trash-* 38 | 39 | #------------------------- 40 | # Environment Files 41 | #------------------------- 42 | # These should never be under version control, 43 | # as it poses a security risk. 44 | .env 45 | .vagrant 46 | application/.env 47 | Vagrantfile 48 | 49 | #------------------------- 50 | # Temporary Files 51 | #------------------------- 52 | writable/cache/* 53 | !writable/cache/index.html 54 | !writable/cache/.htaccess 55 | 56 | writable/logs/* 57 | !writable/logs/index.html 58 | !writable/logs/.htaccess 59 | 60 | writable/session/* 61 | !writable/session/index.html 62 | !writable/session/.htaccess 63 | 64 | writable/uploads/* 65 | !writable/uploads/index.html 66 | !writable/uploads/.htaccess 67 | 68 | writable/debugbar/* 69 | 70 | writable/tmp/* 71 | !writable/tmp/index.html 72 | !writable/tmp/.htaccess 73 | 74 | php_errors.log 75 | 76 | #------------------------- 77 | # User Guide Temp Files 78 | #------------------------- 79 | user_guide_src/build/* 80 | user_guide_src/cilexer/build/* 81 | user_guide_src/cilexer/dist/* 82 | user_guide_src/cilexer/pycilexer.egg-info/* 83 | 84 | #------------------------- 85 | # Test Files 86 | #------------------------- 87 | #tests/coverage* 88 | 89 | # Don't save phpunit under version control. 90 | phpunit 91 | 92 | #------------------------- 93 | # Composer 94 | #------------------------- 95 | vendor/ 96 | composer.lock 97 | 98 | #------------------------- 99 | # IDE / Development Files 100 | #------------------------- 101 | 102 | # Modules Testing 103 | _modules/* 104 | 105 | # phpenv local config 106 | .php-version 107 | 108 | # Jetbrains editors (PHPStorm, etc) 109 | .idea/ 110 | *.iml 111 | 112 | # Netbeans 113 | nbproject/ 114 | nbbuild/ 115 | dist/ 116 | nbdist/ 117 | nbactions.xml 118 | nb-configuration.xml 119 | .nb-gradle/ 120 | 121 | # Sublime Text 122 | *.tmlanguage.cache 123 | *.tmPreferences.cache 124 | *.stTheme.cache 125 | *.sublime-workspace 126 | *.sublime-project 127 | .phpintel 128 | /api/ 129 | 130 | # Visual Studio Code 131 | .vscode/ 132 | 133 | /results/ 134 | /phpunit*.xml 135 | 136 | composer.phar -------------------------------------------------------------------------------- /Commands/ClearCache.php: -------------------------------------------------------------------------------- 1 | clearCache(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /DataMapper/EntityTrait.php: -------------------------------------------------------------------------------- 1 | _getModel()->getPrimaryKey(); 19 | return !empty($this->{$primaryKey}) || (!is_null($this->all) && count($this->all)); 20 | } 21 | 22 | // 23 | 24 | public function find($id = null) { 25 | $entity = $this->_getModel()->find($id); 26 | foreach(get_object_vars($entity) as $name => $value) 27 | $this->{$name} = $value; 28 | return $entity; 29 | } 30 | 31 | // 32 | 33 | // 34 | 35 | /** 36 | * @param Entity|null $related 37 | * @param string|null $relatedField 38 | */ 39 | public function save($related = null, $relatedField = null) { 40 | if($related instanceof Entity) { 41 | if($related->exists()) { 42 | foreach ($related as $relatedItem) { 43 | $this->saveRelation($relatedItem, $relatedField); 44 | } 45 | } 46 | } else { 47 | $result = $this->_getModel()->save($this); 48 | if(!is_bool($result)) 49 | $this->{$this->_getModel()->getPrimaryKey()} = $result; 50 | } 51 | } 52 | 53 | public function insert() { 54 | $result = $this->_getModel()->insert($this); 55 | if(!is_bool($result)) 56 | $this->{$this->_getModel()->getPrimaryKey()} = $result; 57 | } 58 | 59 | /** 60 | * @param Entity $related 61 | * @param string|null $relationName 62 | */ 63 | public function saveRelation($related, $relationName = null) { 64 | if(!$this->exists() |! $related->exists()) return; 65 | 66 | if(!$relationName) $relationName = get_class($related->_getModel()); 67 | 68 | $thisModel = $this->_getModel(); 69 | $relatedModel = $related->_getModel(); 70 | 71 | $relation = $thisModel->getRelation($relationName); 72 | if(empty($relation)) return; 73 | $relation = $relation[0]; 74 | 75 | $relationShipTable = $relation->getRelationShipTable(); 76 | 77 | if($relationShipTable == $thisModel->getTableName()) { 78 | if(in_array($relation->getJoinOtherAs(), $thisModel->getTableFields())) { 79 | 80 | // Check if opposite relation is hasOne 81 | $opposite = $relatedModel->getRelation($relation->getOtherField()); 82 | if(!empty($opposite) && $opposite[0]->getType() == RelationDef::HasOne) { 83 | $related->deleteRelation($this, $relation->getOtherField()); 84 | } 85 | 86 | $this->{$relation->getJoinOtherAs()} = $related->{$relatedModel->getPrimaryKey()}; 87 | $this->save(); 88 | } 89 | } else if($relationShipTable == $relatedModel->getTableName()) { 90 | if(in_array($relation->getJoinSelfAs(), $relatedModel->getTableFields())) { 91 | 92 | // Check if this relation is hasOne 93 | if($relation->getType() == RelationDef::HasOne) { 94 | $this->deleteRelation($related, $relationName); 95 | } 96 | 97 | $related->{$relation->getJoinSelfAs()} = $this->{$thisModel->getPrimaryKey()}; 98 | $related->save(); 99 | } 100 | } else { 101 | 102 | Database::connect() 103 | ->table($relationShipTable) 104 | ->insert([ 105 | $relation->getJoinSelfAs() => $this->{$thisModel->getPrimaryKey()}, 106 | $relation->getJoinOtherAs() => $related->{$relatedModel->getPrimaryKey()} 107 | ]); 108 | 109 | } 110 | 111 | if($thisModel instanceof OrmEventsInterface && $this instanceof Entity && $related instanceof Entity) { 112 | $thisModel->postAddRelation($this, $related); 113 | } 114 | } 115 | 116 | // 117 | 118 | // 119 | 120 | public function deleteAll() { 121 | /** @var Entity $item */ 122 | foreach($this as $item) $item->delete(); 123 | } 124 | 125 | /** 126 | * @param Entity|null $related 127 | */ 128 | public function delete($related = null) { 129 | if($this->exists()) { 130 | if(is_null($related)) { 131 | 132 | $thisModel = $this->_getModel(); 133 | if(in_array('deletion_id', $this->_getModel()->getTableFields())) { 134 | foreach(OrmExtension::$entityNamespace as $entityNamespace) { 135 | $name = $entityNamespace . 'Deletion'; 136 | if(class_exists($name)) { 137 | /** @var Entity $deletion */ 138 | $deletion = new $name(); 139 | $deletion->save(); 140 | $this->deletion_id = $deletion->{$deletion->_getModel()->getPrimaryKey()}; 141 | $this->save(); 142 | break; 143 | } 144 | } 145 | } else 146 | $thisModel->delete($this->{$thisModel->getPrimaryKey()}); 147 | 148 | if($thisModel instanceof OrmEventsInterface && $this instanceof Entity) 149 | $thisModel->postDelete($this); 150 | 151 | } else { 152 | $this->deleteRelation($related); 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * @param Entity|string $related 159 | * @param string|null $relationName 160 | */ 161 | public function deleteRelation($related, $relationName = null) { 162 | if(!$relationName) $relationName = get_class($related->_getModel()); 163 | 164 | $thisModel = $this->_getModel(); 165 | if(is_string($related)) 166 | $relatedModel = new $related(); 167 | else 168 | $relatedModel = $related->_getModel(); 169 | 170 | $relation = $thisModel->getRelation($relationName); 171 | if(empty($relation)) return; 172 | $relation = $relation[0]; 173 | 174 | $relationShipTable = $relation->getRelationShipTable(); 175 | 176 | if($relationShipTable == $thisModel->getTableName()) { 177 | if(in_array($relation->getJoinOtherAs(), $thisModel->getTableFields())) { 178 | $this->{$relation->getJoinOtherAs()} = null; 179 | $this->save(); 180 | } 181 | } else if($relationShipTable == $relatedModel->getTableName()) { 182 | if(in_array($relation->getJoinSelfAs(), $relatedModel->getTableFields())) { 183 | if(is_string($related)) { 184 | // TODO Handle updated and updated_by_id 185 | Database::connect() 186 | ->table($relationShipTable) 187 | ->update( 188 | [$relation->getJoinSelfAs() => 0], 189 | [$relation->getJoinSelfAs() => $this->{$thisModel->getPrimaryKey()}]); 190 | } else { 191 | $related->{$relation->getJoinSelfAs()} = null; 192 | $related->save(); 193 | } 194 | } 195 | } else { 196 | 197 | Database::connect() 198 | ->table($relationShipTable) 199 | ->delete([ 200 | $relation->getJoinSelfAs() => $this->{$thisModel->getPrimaryKey()}, 201 | $relation->getJoinOtherAs() => $related->{$relatedModel->getPrimaryKey()} 202 | ]); 203 | 204 | } 205 | 206 | unset($this->{$relation->getSimpleName()}); 207 | //$this->resetStoredFields(); 208 | 209 | if($thisModel instanceof OrmEventsInterface && $this instanceof Entity && $related instanceof Entity) { 210 | $thisModel->postDeleteRelation($this, $related); 211 | } 212 | } 213 | 214 | // 215 | 216 | // 217 | 218 | public function allToArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false, array $fieldsFilter = null) { 219 | $items = []; 220 | foreach($this as $item) 221 | $items[] = $item->toArray($onlyChanged, $cast, $recursive, $fieldsFilter); 222 | return $items; 223 | } 224 | 225 | public function allToArrayWithFields(array $fieldsFilter = null, bool $onlyChanged = false, bool $cast = true, bool $recursive = false) { 226 | return $this->allToArray($onlyChanged, $cast, $recursive, $fieldsFilter); 227 | } 228 | 229 | public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recursive = false, array $fieldsFilter = null): array { 230 | $item = []; 231 | 232 | // Fields 233 | $fields = ModelDefinitionCache::getFieldData($this->getSimpleName()); 234 | foreach($fields as $fieldData) { 235 | $fieldName = $fieldData->name; 236 | 237 | if(in_array($fieldName, $this->hiddenFields)) { 238 | continue; 239 | } 240 | 241 | if ($fieldsFilter != null && !in_array($fieldName, $fieldsFilter)) { 242 | continue; 243 | } 244 | 245 | $field = $this->{$fieldName}; 246 | if (is_string($field)) { 247 | switch($fieldData->type) { 248 | case 'bigint': 249 | case 'int': 250 | $item[$fieldName] = is_null($this->{$fieldName}) ? null : (int)$this->{$fieldName}; 251 | break; 252 | case 'float': 253 | case 'double': 254 | case 'decimal': 255 | $item[$fieldName] = (double)$this->{$fieldName}; 256 | break; 257 | case 'tinyint': 258 | $item[$fieldName] = (bool)$this->{$fieldName}; 259 | break; 260 | case 'varchar': 261 | case 'text': 262 | case 'time': 263 | $item[$fieldName] = (string)$this->{$fieldName}; 264 | break; 265 | case 'datetime': 266 | if($this->{$fieldName} != null && $this->{$fieldName} != "0000-00-00 00:00:00") { 267 | $item[$fieldName] = (string)strtotime($this->{$fieldName}); 268 | try { 269 | $foo = new DateTime($this->{$fieldName}, new DateTimeZone("Europe/Copenhagen")); 270 | $foo->setTimeZone(new DateTimeZone("UTC")); 271 | $item[$fieldName] = $foo->format('c'); 272 | } catch(\Exception $e) { 273 | 274 | } 275 | } else $item[$fieldName] = null; 276 | break; 277 | default: 278 | $item[$fieldName] = $this->{$fieldName}; 279 | } 280 | } else { 281 | $item[$fieldName] = $this->{$fieldName}; 282 | } 283 | } 284 | 285 | // Relations 286 | /** @var RelationDef[] $relations */ 287 | $relations = ModelDefinitionCache::getRelations($this->getSimpleName()); 288 | foreach($relations as $relation) { 289 | $fieldName = $relation->getSimpleName(); 290 | switch($relation->getType()) { 291 | case RelationDef::HasOne: 292 | if(isset($this->{$fieldName}) && $this->{$fieldName}->exists()) 293 | $item[$fieldName] = $this->{$fieldName}->toArray(); 294 | break; 295 | case RelationDef::HasMany: 296 | $fieldName = plural($fieldName); 297 | if(isset($this->{$fieldName}) && $this->{$fieldName}->exists()) 298 | $item[$fieldName] = $this->{$fieldName}->allToArray(); 299 | break; 300 | } 301 | } 302 | 303 | 304 | return $item; 305 | } 306 | 307 | // 308 | 309 | // 310 | 311 | public function count() { 312 | return !is_null($this->all) ? count($this->all) : ($this->exists() ? 1 : 0); 313 | } 314 | 315 | public function add($item) { 316 | if(is_null($this->all)) $this->all = []; 317 | $this->all[] = $item; 318 | $this->idMap = null; 319 | } 320 | 321 | public function remove($item) { 322 | if(is_null($this->all)) $this->all = []; 323 | if(($key = array_search($item, $this->all)) !== false) { 324 | unset($this->all[$key]); 325 | } 326 | $this->idMap = null; 327 | } 328 | 329 | public function removeById($id) { 330 | $item = $this->getById($id); 331 | if($item) $this->remove($item); 332 | } 333 | 334 | private $idMap = null; // Initialized when needed 335 | private function initIdMap() { 336 | $this->idMap = []; 337 | $primaryKey = $this->_getModel()->getPrimaryKey(); 338 | foreach($this as $item) $this->idMap[$item->{$primaryKey}] = $item; 339 | } 340 | public function getById($id) { 341 | if(is_null($this->idMap)) $this->initIdMap(); 342 | return isset($this->idMap[$id]) ? $this->idMap[$id] : null; 343 | } 344 | 345 | public function hasId($id) { 346 | if(is_null($this->idMap)) $this->initIdMap(); 347 | return isset($this->idMap[$id]); 348 | } 349 | 350 | public function clear() { 351 | $this->all = []; 352 | } 353 | 354 | /** 355 | * @return ArrayIterator 356 | */ 357 | public function getIterator(): ArrayIterator { 358 | return new ArrayIterator(!is_null($this->all) ? $this->all : ($this->exists() ? [$this] : [])); 359 | } 360 | 361 | // 362 | 363 | 364 | public function getTableFields() { 365 | return $this->_getModel()->getTableFields(); 366 | } 367 | 368 | public function resetStoredFields() { 369 | foreach($this->getTableFields() as $field) { 370 | $this->stored[$field] = $this->{$field}; 371 | } 372 | } 373 | 374 | } 375 | -------------------------------------------------------------------------------- /DataMapper/ModelDefinitionCache.php: -------------------------------------------------------------------------------- 1 | cache)) { 25 | static::$instance->init(); 26 | } 27 | return static::$instance; 28 | } 29 | 30 | private $config; 31 | 32 | public $cache; 33 | 34 | public function init() { 35 | $this->config = new Cache(); 36 | $this->cache = Services::cache($this->config, false); 37 | } 38 | 39 | 40 | public static function setFields($entity, $fields) { 41 | static::setData($entity.'_fields', $fields); 42 | } 43 | 44 | public static function getFields($entity, $tableName = null) { 45 | $fields = static::getData($entity.'_fields'); 46 | if(is_null($fields)) { 47 | $fieldData = ModelDefinitionCache::getFieldData($entity, $tableName); 48 | $fields = []; 49 | if($fieldData) { 50 | foreach($fieldData as $field) $fields[] = $field->name; 51 | } 52 | ModelDefinitionCache::setFields($entity, $fields); 53 | } 54 | return $fields; 55 | } 56 | 57 | public static function setFieldData($entity, $fields) { 58 | static::setData($entity.'_field_data', $fields); 59 | } 60 | 61 | public static function getFieldData($entity, $tableName = null) { 62 | $fieldData = static::getData($entity.'_field_data'); 63 | if(is_null($fieldData)) { 64 | if(is_null($tableName)) { 65 | 66 | foreach(OrmExtension::$modelNamespace as $modelNamespace) { 67 | $modelName = $modelNamespace . $entity . 'Model'; 68 | if(class_exists($modelName)) { 69 | /** @var Model $model */ 70 | $model = new $modelName(); 71 | $tableName = $model->getTableName(); 72 | } 73 | } 74 | } 75 | 76 | $db = Database::connect(); 77 | try { 78 | $fieldData = $db->getFieldData($tableName); 79 | ModelDefinitionCache::setFieldData($entity, $fieldData); 80 | } catch(\Exception $e) { 81 | // Ignore table doesn't exist 82 | if($e->getCode() != 1146) { 83 | throw $e; 84 | } 85 | } 86 | } 87 | return $fieldData; 88 | } 89 | 90 | public static function setRelations($entity, $relations) { 91 | static::setData($entity.'_relations', $relations); 92 | } 93 | 94 | /** 95 | * @param $entity 96 | * @return RelationDef[] 97 | * @throws \Exception 98 | */ 99 | public static function getRelations($entity) { 100 | $relations = static::getData($entity.'_relations'); 101 | if (!$relations) { 102 | foreach (OrmExtension::$modelNamespace as $modelNamespace) { 103 | $modelName = $modelNamespace . $entity . 'Model'; 104 | if (class_exists($modelName)) { 105 | /** @var Model $model */ 106 | $model = new $modelName(); 107 | break; 108 | } 109 | } 110 | $relations = []; 111 | if (isset($model)) { 112 | foreach ($model->hasOne as $name => $hasOne) { 113 | $relations[] = new RelationDef($model, $name, $hasOne, RelationDef::HasOne); 114 | } 115 | foreach ($model->hasMany as $name => $hasMany) { 116 | $relations[] = new RelationDef($model, $name, $hasMany, RelationDef::HasMany); 117 | } 118 | } 119 | ModelDefinitionCache::setRelations($entity, $relations); 120 | } 121 | return $relations; 122 | } 123 | 124 | 125 | 126 | 127 | 128 | private static function setData($name, $data, $ttl = YEAR) { 129 | $instance = ModelDefinitionCache::getInstance(); 130 | if (isset($instance->cache)) { 131 | $instance->cache->save($name, $data, $ttl); 132 | 133 | // Change file permissions so other users can read and write 134 | $cacheInfo = $instance->cache->getCacheInfo(); 135 | if (isset($cacheInfo[$name])) { 136 | chmod($cacheInfo[$name]['server_path'], 0775); 137 | } 138 | } 139 | } 140 | 141 | private $memcache = []; 142 | private static function getData($name) { 143 | try { 144 | $instance = ModelDefinitionCache::getInstance(); 145 | if(!isset($instance->memcache[$name])) { 146 | $data = $instance->cache->get($name); 147 | if($data) $instance->memcache[$name] = $data; 148 | return $data; 149 | } else { 150 | return $instance->memcache[$name]; 151 | } 152 | } catch(\Exception $e) { 153 | return null; 154 | } 155 | } 156 | 157 | public function clearCache($rmDir = false) { 158 | $this->memcache = []; 159 | $this->cache->clean(); 160 | if ($rmDir && is_dir($this->config->storePath)) { 161 | rmdir($this->config->storePath); 162 | } 163 | } 164 | 165 | 166 | } 167 | -------------------------------------------------------------------------------- /DataMapper/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | /** 13 | * @param string|array $relationName 14 | * @param null $fields 15 | * @return Model 16 | */ 17 | public function includeRelated($relationName, $fields = null, bool $withDeleted = false): Model { 18 | $parent = $this->_getModel(); 19 | $relations = $this->getRelation($relationName); 20 | 21 | // Handle deep relations 22 | $last = $this; 23 | $table = null; 24 | $prefix = ''; 25 | $relationPrefix = ''; 26 | foreach ($relations as $relation) { 27 | $table = $last->addRelatedTable($relation, $prefix, $table, $withDeleted); 28 | $prefix .= plural($relation->getSimpleName()) . '_'; 29 | $relationPrefix .= plural($relation->getSimpleName()) . '/'; 30 | 31 | // Prepare next 32 | $builder = $last->_getBuilder(); 33 | $selecting = $last->isSelecting(); 34 | $relatedTablesAdded =& $last->relatedTablesAdded; 35 | $last = $relation->getRelationClass(); 36 | $last->_setBuilder($builder); 37 | $last->setSelecting($selecting); 38 | $last->relatedTablesAdded =& $relatedTablesAdded; 39 | 40 | if (!$parent->hasIncludedRelation($relationPrefix)) { 41 | $parent->addIncludedRelation($relationPrefix, $relation); 42 | $this->selectIncludedRelated($parent, $relation, $table, $prefix, $fields); 43 | } 44 | } 45 | 46 | return $parent; 47 | } 48 | 49 | /** 50 | * @param Model $parent 51 | * @param RelationDef $relation 52 | * @param string $table 53 | * @param string $prefix 54 | * @param null $fields 55 | */ 56 | private function selectIncludedRelated($parent, $relation, $table, $prefix, $fields = null) { 57 | $selection = []; 58 | 59 | $relationClassName = $relation->getClass(); 60 | /** @var Model $related */ 61 | $related = new $relationClassName(); 62 | 63 | if (is_null($fields)) $fields = $related->getTableFields(); 64 | foreach ($fields as $field) { 65 | $new_field = $prefix . $field; 66 | 67 | // Prevent collisions 68 | if (in_array($new_field, $parent->getTableFields())) continue; 69 | 70 | $selection[] = "{$table}.{$field} AS {$new_field}"; 71 | } 72 | 73 | $this->select(implode(', ', $selection)); 74 | } 75 | 76 | // 77 | 78 | // 79 | 80 | public function whereRelated($relationName, $field, $value, $escape = null): Model { 81 | $table = $this->handleWhereRelated($relationName); 82 | $model = $this->_getModel(); 83 | $model->where("{$table[0]}.{$field}", $value, $escape, false); 84 | return $model; 85 | } 86 | 87 | public function whereInRelated($relationName, $field, $value, $escape = null): Model { 88 | $table = $this->handleWhereRelated($relationName); 89 | $model = $this->_getModel(); 90 | $model->whereIn("{$table[0]}.{$field}", $value, $escape, false); 91 | return $model; 92 | } 93 | 94 | public function whereNotInRelated($relationName, $field, $value, $escape = null): Model { 95 | $table = $this->handleWhereRelated($relationName); 96 | $model = $this->_getModel(); 97 | $model->whereNotIn("{$table[0]}.{$field}", $value, $escape, false); 98 | return $model; 99 | } 100 | 101 | public function whereBetweenRelated($relationName, $field, $min, $max, $escape = null): Model { 102 | $table = $this->handleWhereRelated($relationName); 103 | $model = $this->_getModel(); 104 | $model->whereBetween("{$table[0]}.{$field}", $min, $max, $escape, false); 105 | return $model; 106 | } 107 | 108 | public function whereNotBetweenRelated($relationName, $field, $min, $max, $escape = false): Model { 109 | $table = $this->handleWhereRelated($relationName); 110 | $model = $this->_getModel(); 111 | $model->whereNotBetween("{$table[0]}.{$field}", $min, $max, $escape, false); 112 | return $model; 113 | } 114 | 115 | public function orWhereRelated($relationName, $field, $value, $escape = null): Model { 116 | $table = $this->handleWhereRelated($relationName); 117 | $model = $this->_getModel(); 118 | $model->orWhere("{$table[0]}.{$field}", $value, $escape, false); 119 | return $model; 120 | } 121 | 122 | public function orWhereInRelated($relationName, $field, $value, $escape = null): Model { 123 | $table = $this->handleWhereRelated($relationName); 124 | $model = $this->_getModel(); 125 | $model->orWhereIn("{$table[0]}.{$field}", $value, $escape, false); 126 | return $model; 127 | } 128 | 129 | public function orWhereNotInRelated($relationName, $field, $value, $escape = null): Model { 130 | $table = $this->handleWhereRelated($relationName); 131 | $model = $this->_getModel(); 132 | $model->orWhereNotIn("{$table[0]}.{$field}", $value, $escape, false); 133 | return $model; 134 | } 135 | 136 | // 137 | 138 | // 139 | 140 | public function likeRelated($relationName, $field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false, bool $withDeleted = false): Model { 141 | $table = $this->handleWhereRelated($relationName, $withDeleted); 142 | $model = $this->_getModel(); 143 | $model->like("{$table[0]}.{$field}", $match, $side, $escape, $insensitiveSearch, false); 144 | return $model; 145 | } 146 | 147 | public function notLikeRelated($relationName, $field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false, bool $withDeleted = false): Model { 148 | $table = $this->handleWhereRelated($relationName, $withDeleted); 149 | $model = $this->_getModel(); 150 | $model->notLike("{$table[0]}.{$field}", $match, $side, $escape, $insensitiveSearch, false); 151 | return $model; 152 | } 153 | 154 | public function orLikeRelated($relationName, $field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false, bool $withDeleted = false): Model { 155 | $table = $this->handleWhereRelated($relationName, $withDeleted); 156 | $model = $this->_getModel(); 157 | $model->orLike("{$table[0]}.{$field}", $match, $side, $escape, $insensitiveSearch, false); 158 | return $model; 159 | } 160 | 161 | public function orNotLikeRelated($relationName, $field, $match = '', $side = 'both', $escape = null, $insensitiveSearch = false, bool $withDeleted = false): Model { 162 | $table = $this->handleWhereRelated($relationName, $withDeleted); 163 | $model = $this->_getModel(); 164 | $model->orNotLike("{$table[0]}.{$field}", $match, $side, $escape, $insensitiveSearch, false); 165 | return $model; 166 | } 167 | 168 | // 169 | 170 | // 171 | 172 | /** 173 | * @param Model $query 174 | * @param string $alias 175 | * @return Model 176 | */ 177 | public function selectSubQuery($query, $alias) { 178 | $model = $this->_getModel(); 179 | $query = $this->parseSubQuery($query); 180 | $model->select("{$query} AS {$alias}"); 181 | return $model; 182 | } 183 | 184 | /** 185 | * @param Model $query 186 | * @param null $value 187 | * @return Model 188 | */ 189 | public function whereSubQuery($query, $operator, $value = null, $escape = null) { 190 | $model = $this->_getModel(); 191 | $field = $this->parseSubQuery($query); 192 | $model->where("{$field} {$operator}", $value, $escape, false); 193 | return $model; 194 | } 195 | 196 | /** 197 | * @param Model $query 198 | * @param null $value 199 | * @return Model 200 | */ 201 | public function orWhereSubQuery($query, $operator, $value = null, $escape = null) { 202 | $model = $this->_getModel(); 203 | $field = $this->parseSubQuery($query); 204 | $model->orWhere("{$field} {$operator}", $value, $escape, false); 205 | return $model; 206 | } 207 | 208 | public function orderBySubQuery($query, $direction = '', $escape = null) { 209 | $model = $this->_getModel(); 210 | $field = $this->parseSubQuery($query); 211 | $model->orderBy($field, $direction, $escape, false); 212 | return $model; 213 | } 214 | 215 | /** 216 | * @param Model $query 217 | * @return mixed 218 | */ 219 | protected function parseSubQuery($query) { 220 | $model = $this->_getModel(); 221 | 222 | $query->bindReplace('${parent}', $this->getTableName()); 223 | 224 | $sql = $query->compileSelect_(); 225 | $sql = "({$sql})"; 226 | 227 | // Data::sql($sql); 228 | 229 | $sql = $this->bindMerging($sql, $query->getBindKeyCount(), $query->getBinds()); 230 | 231 | $tableName = $model->db->protectIdentifiers($model->getTableName()); 232 | $tableNameThisQuote = preg_quote($model->getTableName()); 233 | $tableNameQuote = preg_quote($tableName); 234 | $tablePattern = "(?:{$tableNameThisQuote}|{$tableNameQuote}|\({$tableNameQuote}\))"; 235 | 236 | $fieldName = $model->db->protectIdentifiers('__field__'); 237 | $fieldName = str_replace('__field__', '[-\w]+', preg_quote($fieldName)); 238 | $fieldPattern = "([-\w]+|{$fieldName})"; 239 | 240 | // Pattern ends up being [^_](table|`table`).(field|`field`) 241 | $pattern = "/([^_:]){$tablePattern}\.{$fieldPattern}/i"; 242 | 243 | // Replacement ends up being `table_subquery`.`$1` 244 | $tableSubQueryName = $model->db->protectIdentifiers($model->getTableName() . '_subquery'); 245 | $replacement = "$1{$tableSubQueryName}.$2"; 246 | $sql = preg_replace($pattern, $replacement, $sql); 247 | 248 | // Replace all "table table" aliases 249 | $pattern = "/{$tablePattern} {$tablePattern} /i"; 250 | $replacement = "{$tableName} {$tableSubQueryName} "; 251 | $sql = preg_replace($pattern, $replacement, $sql); 252 | 253 | // Replace "FROM table" for self relationships 254 | $pattern = "/FROM {$tablePattern}([,\\s])/i"; 255 | $replacement = "FROM {$tableName} $tableSubQueryName$1"; 256 | $sql = preg_replace($pattern, $replacement, $sql); 257 | $sql = str_replace("\n", " ", $sql); 258 | 259 | // Data::sql($sql); 260 | 261 | return str_replace('${parent}', $this->getTableName(), $sql); 262 | } 263 | 264 | // 265 | 266 | // 267 | 268 | public function groupByRelated($relationName, $by, $escape = null): Model { 269 | $table = $this->handleWhereRelated($relationName); 270 | $model = $this->_getModel(); 271 | $model->groupBy("{$table[0]}.{$by}", $escape, false); 272 | return $model; 273 | } 274 | 275 | public function havingRelated($relationName, $key, $value, $escape = null): Model { 276 | $table = $this->handleWhereRelated($relationName); 277 | $model = $this->_getModel(); 278 | $model->having("{$table[0]}.{$key}", $value, $escape, false); 279 | return $model; 280 | } 281 | 282 | public function orHavingRelated($relationName, $key, $value, $escape = null): Model { 283 | $table = $this->handleWhereRelated($relationName); 284 | $model = $this->_getModel(); 285 | $model->orHaving("{$table[0]}.{$key}", $value, $escape, false); 286 | return $model; 287 | } 288 | 289 | public function orderByRelated($relationName, $orderby, $direction = '', $escape = null): Model { 290 | $table = $this->handleWhereRelated($relationName); 291 | $model = $this->_getModel(); 292 | $model->orderBy("{$table[0]}.{$orderby}", $direction, $escape, false); 293 | return $model; 294 | } 295 | 296 | 297 | // 298 | 299 | 300 | // 301 | 302 | public function getTableFields() { 303 | return $this->_getModel()->allowedFields; 304 | } 305 | 306 | // 307 | 308 | // 309 | 310 | public function handleWhereRelated($relationName, bool $withDeleted = false) { 311 | $relations = $this->getRelation($relationName); 312 | 313 | // Handle deep relations 314 | $last = $this; 315 | $table = null; 316 | /** @var RelationDef $relation */ 317 | $relation = null; 318 | $prefix = ''; 319 | foreach ($relations as $relation) { 320 | $table = $last->addRelatedTable($relation, $prefix, $table, $withDeleted); 321 | $prefix .= plural($relation->getSimpleName()) . '_'; 322 | 323 | // Prepare next 324 | $builder = $last->_getBuilder(); 325 | $selecting = $last->isSelecting(); 326 | $relatedTablesAdded =& $last->relatedTablesAdded; 327 | $last = $relation->getRelationClass(); 328 | $last->_setBuilder($builder); 329 | $last->setSelecting($selecting); 330 | $last->relatedTablesAdded =& $relatedTablesAdded; 331 | // Data::debug(get_class($last), $table); 332 | } 333 | 334 | return [$table, $last]; 335 | } 336 | 337 | private $relatedTablesAdded = []; 338 | 339 | /** 340 | * @param RelationDef $relation 341 | * @param string $prefix 342 | * @param string $this_table 343 | * @return string 344 | */ 345 | public function addRelatedTable(RelationDef $relation, $prefix = '', $this_table = null, bool $withDeleted = false) { 346 | if (!$this_table) { 347 | $this_table = $this->getTableName(); 348 | } 349 | //Data::debug("QueryBuilder::addRelatedTable", 'Name='.$relation->getSimpleName(), 'Prefix='.$prefix, 'Table='.$this_table); 350 | 351 | $related = $relation->getRelationClass(); 352 | $relationShipTable = $relation->getRelationShipTable(); 353 | 354 | // If no selects, select this table 355 | if (!$this->isSelecting()) { 356 | $this->select($this->getTableName() . '.*'); 357 | } 358 | 359 | $addSoftDeletionCondition = !$withDeleted && $related->useSoftDeletes; 360 | $deletedField = $related->deletedField; 361 | 362 | if (($relation->getClass() == $relation->getName()) && ($this->getTableName() != $related->getTableName())) { 363 | $prefixedParentTable = $prefix . $related->getTableName(); 364 | $prefixedRelatedTable = $prefix . $relationShipTable; 365 | } else { // Used when relation is custom named 366 | $prefixedParentTable = $prefix . plural($relation->getSimpleName()) . '_' . $related->getTableName(); 367 | $prefixedRelatedTable = $prefix . plural($relation->getSimpleName()) . '_' . $relationShipTable; 368 | } 369 | 370 | if ($relationShipTable == $this->getTableName() && in_array($relation->getJoinOtherAs(), $this->getTableFields())) { 371 | 372 | foreach ([$relation->getJoinSelfAs(), 'id'] as $joinSelfAs) { 373 | if (in_array($joinSelfAs, $related->getTableFields())) { 374 | if (!in_array($prefixedParentTable, $this->relatedTablesAdded)) { 375 | $cond = "{$prefixedParentTable}.{$joinSelfAs} = {$this_table}.{$relation->getJoinOtherAs()}"; 376 | if ($addSoftDeletionCondition) { 377 | $cond .= " AND {$prefixedParentTable}.{$deletedField} IS NULL"; 378 | } 379 | $this->join("{$related->getTableName()} {$prefixedParentTable}", $cond, 'LEFT OUTER'); 380 | 381 | $this->relatedTablesAdded[] = $prefixedParentTable; 382 | } 383 | $match = $prefixedParentTable; 384 | break; 385 | } 386 | } 387 | 388 | } else if ($relationShipTable == $related->getTableName() && in_array($relation->getJoinSelfAs(), $related->getTableFields())) { 389 | 390 | foreach ([$relation->getJoinOtherAs(), 'id'] as $joinOtherAs) { 391 | if (in_array($joinOtherAs, $this->getTableFields())) { 392 | if (!in_array($prefixedParentTable, $this->relatedTablesAdded)) { 393 | $cond = "{$this_table}.{$joinOtherAs} = {$prefixedParentTable}.{$relation->getJoinSelfAs()}"; 394 | if ($addSoftDeletionCondition) { 395 | $cond .= " AND {$prefixedParentTable}.{$deletedField} IS NULL"; 396 | } 397 | $this->join("{$related->getTableName()} {$prefixedParentTable}", $cond, 'LEFT OUTER'); 398 | 399 | $this->relatedTablesAdded[] = $prefixedParentTable; 400 | } 401 | $match = $prefixedParentTable; 402 | break; 403 | } 404 | } 405 | 406 | } else { 407 | 408 | // Use a join table. We have to do two joins now. First the join table and then the relation table. 409 | 410 | $joinMatch = null; 411 | $match = null; 412 | foreach ($relation->getJoinOtherAsGuess() as $joinOtherAs) { 413 | if (in_array($joinOtherAs, $this->getTableFields())) { 414 | if (!in_array($prefixedRelatedTable, $this->relatedTablesAdded)) { 415 | $cond = "{$this_table}.{$joinOtherAs} = {$prefixedRelatedTable}.{$relation->getJoinSelfAs()}"; 416 | $this->join("{$relationShipTable} {$prefixedRelatedTable}", $cond, 'LEFT OUTER'); 417 | 418 | $this->relatedTablesAdded[] = $prefixedRelatedTable; 419 | } 420 | $joinMatch = $prefixedRelatedTable; 421 | 422 | // Second join 423 | if (!in_array($prefixedParentTable, $this->relatedTablesAdded)) { 424 | $cond = "{$prefixedParentTable}.{$related->getPrimaryKey()} = {$prefixedRelatedTable}.{$relation->getJoinOtherAs()}"; 425 | if ($addSoftDeletionCondition) { 426 | $cond .= " AND {$prefixedParentTable}.{$deletedField} IS NULL"; 427 | } 428 | $this->join("{$related->getTableName()} {$prefixedParentTable}", $cond, 'LEFT OUTER'); 429 | 430 | $this->relatedTablesAdded[] = $prefixedParentTable; 431 | } 432 | $match = $prefixedParentTable; 433 | break; 434 | } 435 | } 436 | 437 | // If we still have not found a match, this is probably a custom join table 438 | if (is_null($joinMatch)) { 439 | if (!in_array($prefixedRelatedTable, $this->relatedTablesAdded)) { 440 | $cond = "{$this_table}.{$this->getPrimaryKey()} = {$prefixedRelatedTable}.{$relation->getJoinSelfAs()}"; 441 | $this->join("{$relationShipTable} {$prefixedRelatedTable}", $cond, 'LEFT OUTER'); 442 | $this->relatedTablesAdded[] = $prefixedRelatedTable; 443 | } 444 | } 445 | 446 | if (is_null($match)) { 447 | if (!in_array($prefixedParentTable, $this->relatedTablesAdded)) { 448 | $cond = "{$prefixedParentTable}.{$related->getPrimaryKey()} = {$prefixedRelatedTable}.{$relation->getJoinOtherAs()}"; 449 | $this->join("{$related->getTableName()} {$prefixedParentTable}", $cond, 'LEFT OUTER'); 450 | $this->relatedTablesAdded[] = $prefixedParentTable; 451 | } 452 | $match = $prefixedParentTable; 453 | } 454 | 455 | } 456 | 457 | return $match ?? ''; 458 | } 459 | 460 | /** 461 | * @param string|array $name 462 | * @param bool $useSimpleName 463 | * @return RelationDef[] $result 464 | * @throws \Exception 465 | */ 466 | public function getRelation($name, $useSimpleName = false) { 467 | // Handle deep relations 468 | if (is_array($name)) { 469 | $last = $this; 470 | $result = []; 471 | foreach ($name as $ref) { 472 | $relations = $last->getRelation($ref, $useSimpleName); 473 | if (count($relations) == 0) { 474 | throw new \Exception("Failed to find relation $name for " . get_class($this)); 475 | } 476 | $relation = $relations[0]; 477 | $last = $relation->getRelationClass(); 478 | $result[] = $relation; 479 | } 480 | return $result; 481 | } 482 | 483 | foreach ($this->getRelations() as $relation) { 484 | if ($useSimpleName) { 485 | if ($relation->getSimpleName() == $name) return [$relation]; 486 | } else { 487 | if ($relation->getName() == $name) return [$relation]; 488 | } 489 | } 490 | 491 | throw new \Exception("Failed to find relation $name for " . get_class($this)); 492 | } 493 | 494 | /** 495 | * @return RelationDef[] 496 | */ 497 | public function getRelations() { 498 | $entityName = $this->_getModel()->getEntityName(); 499 | $relations = ModelDefinitionCache::getRelations($entityName); 500 | return $relations; 501 | } 502 | 503 | // 504 | 505 | } 506 | -------------------------------------------------------------------------------- /DataMapper/QueryBuilderInterface.php: -------------------------------------------------------------------------------- 1 | setParentClass($model); 35 | $this->setType($type); 36 | if (is_string($data)) { 37 | $this->setName($data); 38 | $this->setClass($data); 39 | } else if (is_array($data)) { 40 | $this->setName($name); 41 | if (isset($data['class'])) { 42 | $this->setClass($data['class']); 43 | } else { 44 | $this->setClass($name); 45 | } 46 | if (isset($data['otherField'])) { 47 | $this->setOtherField($data['otherField']); 48 | } 49 | if (isset($data['joinSelfAs'])) { 50 | $this->setJoinSelfAs($data['joinSelfAs']); 51 | } else if ($type == self::HasOne) { 52 | $this->setJoinSelfAs("{$name}_{$model->getPrimaryKey()}"); 53 | } 54 | if (isset($data['joinOtherAs'])) { 55 | $this->setJoinOtherAs($data['joinOtherAs']); 56 | } 57 | if (isset($data['joinTable'])) { 58 | $this->setJoinTable($data['joinTable']); 59 | } 60 | if (isset($data['cascadeDelete'])) { 61 | $this->setCascadeDelete($data['cascadeDelete']); 62 | } 63 | } 64 | 65 | if (!isset($this->otherField)) { 66 | $this->setOtherField(get_class($model)); 67 | } 68 | if(!isset($this->joinSelfAs)) { 69 | $this->setJoinSelfAs("{$this->getSimpleOtherField()}_{$model->getPrimaryKey()}"); 70 | } 71 | if(!isset($this->joinOtherAs)) { 72 | $this->setJoinOtherAs("{$this->getSimpleName()}_{$model->getPrimaryKey()}"); 73 | } 74 | // Data::debug('Def:', get_class($this->getParent()), $this->getName(), $this->getJoinSelfAs(), $this->getJoinOtherAs()); 75 | 76 | if (!isset($this->joinTable)) { 77 | $relationClassName = $this->getClass(); 78 | /** @var Model $relationClass */ 79 | $relationClass = new $relationClassName(); 80 | if (!$relationClass instanceof Model) { 81 | throw new Exception("Invalid relation {$this->getName()} for " . get_class($model)); 82 | } 83 | $joins = [$model->getTableName(), $relationClass->getTableName()]; 84 | sort($joins); 85 | $this->setJoinTable(strtolower(implode('_', $joins))); 86 | } 87 | } 88 | 89 | public function getRelationShipTable() { 90 | $model = $this->getRelationShipTableModel(); 91 | return $model ? $model->getTableName() : $this->getJoinTable(); 92 | } 93 | 94 | public function getRelationShipTableModel() { 95 | $parent = $this->getParent(); 96 | $related = $this->getRelationClass(); 97 | 98 | // See if the relationship is in parent table 99 | if (array_key_exists($this->getName(), $parent->hasOne) 100 | || in_array($this->getName(), $parent->hasOne)) { 101 | if (in_array($this->getJoinOtherAs(), $parent->getTableFields())) { 102 | return $parent; 103 | } 104 | } 105 | 106 | if (array_key_exists($this->getOtherField(), $related->hasOne) 107 | || in_array($this->getOtherField(), $related->hasOne)) { 108 | if (in_array($this->getJoinSelfAs(), $related->getTableFields())) 109 | return $related; 110 | } 111 | 112 | // No? Then it must be a join table 113 | return null; 114 | } 115 | 116 | private $relationClass; 117 | 118 | public function getRelationClass(): Model { 119 | if (is_null($this->relationClass)) { 120 | $className = $this->getClass(); 121 | $this->relationClass = new $className(); 122 | } 123 | return $this->relationClass; 124 | } 125 | 126 | public function getSimpleName() { 127 | $namespace = explode('\\', $this->getName()); 128 | $trim = strtolower(preg_replace('/(?getClass()), 0, -5); 137 | } 138 | 139 | public function getSimpleOtherField() { 140 | $namespace = explode('\\', $this->getOtherField()); 141 | $trim = strtolower(preg_replace('/(? 149 | 150 | /** 151 | * @return string 152 | */ 153 | public function getName(): string { 154 | return $this->name; 155 | } 156 | 157 | /** 158 | * @param string $name 159 | */ 160 | public function setName(string $name): void { 161 | $this->name = $name; 162 | } 163 | 164 | /** 165 | * @return string 166 | */ 167 | public function getClass(): string { 168 | return $this->class; 169 | } 170 | 171 | /** 172 | * @param string $class 173 | */ 174 | public function setClass(string $class): void { 175 | $this->class = $class; 176 | } 177 | 178 | /** 179 | * @return string 180 | */ 181 | public function getOtherField(): string { 182 | return $this->otherField; 183 | } 184 | 185 | /** 186 | * @param string $otherField 187 | */ 188 | public function setOtherField(string $otherField): void { 189 | $this->otherField = $otherField; 190 | } 191 | 192 | /** 193 | * @return string 194 | */ 195 | public function getJoinSelfAs(): string { 196 | return $this->joinSelfAs; 197 | } 198 | 199 | /** 200 | * @param string $joinSelfAs 201 | */ 202 | public function setJoinSelfAs(string $joinSelfAs): void { 203 | $this->joinSelfAs = $joinSelfAs; 204 | } 205 | 206 | public function getJoinSelfAsGuess(): array { 207 | return [ 208 | $this->getJoinSelfAs(), 209 | $this->getParent()->getPrimaryKey(), 210 | ]; 211 | } 212 | 213 | /** 214 | * @return string 215 | */ 216 | public function getJoinOtherAs(): string { 217 | return $this->joinOtherAs; 218 | } 219 | 220 | /** 221 | * @param string $joinOtherAs 222 | */ 223 | public function setJoinOtherAs(string $joinOtherAs): void { 224 | $this->joinOtherAs = $joinOtherAs; 225 | } 226 | 227 | public function getJoinOtherAsGuess(): array { 228 | return [ 229 | $this->getJoinOtherAs(), 230 | $this->getParent()->getPrimaryKey(), 231 | ]; 232 | } 233 | 234 | /** 235 | * @return string 236 | */ 237 | public function getJoinTable(): string { 238 | return $this->joinTable; 239 | } 240 | 241 | /** 242 | * @param string $joinTable 243 | */ 244 | public function setJoinTable(string $joinTable): void { 245 | $this->joinTable = $joinTable; 246 | } 247 | 248 | /** 249 | * @return bool 250 | */ 251 | public function isCascadeDelete(): bool { 252 | return $this->cascadeDelete; 253 | } 254 | 255 | /** 256 | * @param bool $cascadeDelete 257 | */ 258 | public function setCascadeDelete(bool $cascadeDelete): void { 259 | $this->cascadeDelete = $cascadeDelete; 260 | } 261 | 262 | /** 263 | * @return int 264 | */ 265 | public function getType(): int { 266 | return $this->type; 267 | } 268 | 269 | /** 270 | * @param int $type 271 | */ 272 | public function setType(int $type): void { 273 | $this->type = $type; 274 | } 275 | 276 | /** 277 | * @return Model 278 | */ 279 | public function getParent(): Model { 280 | return new $this->parentClass; 281 | } 282 | 283 | /** 284 | * @param Model $parent 285 | */ 286 | public function setParentClass(Model $parent): void { 287 | $this->parentClass = get_class($parent); 288 | } 289 | 290 | // 291 | } 292 | -------------------------------------------------------------------------------- /DataMapper/ResultBuilder.php: -------------------------------------------------------------------------------- 1 | includedRelations[$fullName] = $relation; 22 | } 23 | 24 | protected function hasIncludedRelation(string $fullName): bool { 25 | return isset($this->includedRelations[$fullName]); 26 | } 27 | 28 | /** 29 | * @param Entity[] $result 30 | */ 31 | protected function arrangeIncludedRelations(&$result) { 32 | //Data::debug(get_class($this), "arrangeIncludedRelations for", count($result), 'entities with', count($this->includedRelations), 'relations'); 33 | 34 | $relations = $this->includedRelations; 35 | ksort($relations); 36 | 37 | foreach($result as $row) { 38 | //$row->resetStoredFields(); // TODO Brug CI's 39 | 40 | foreach($relations as $relationPrefix => $relation) { 41 | $fullName = str_replace('/', '_', $relationPrefix); 42 | 43 | // Deep relation 44 | $current = $row; 45 | $deepRelations = explode('/', trim($relationPrefix, '/')); 46 | array_pop($deepRelations); 47 | foreach($deepRelations as $prefix) { 48 | if($relation->getType() == RelationDef::HasOne) { 49 | $current = $current->{singular($prefix)}; 50 | } else { 51 | $current = $current->{$prefix}; 52 | } 53 | } 54 | 55 | $entityName = $relation->getEntityName(); 56 | /** @var Entity $entity */ 57 | $entity = new $entityName(); 58 | 59 | $attributes = []; 60 | foreach($relation->getRelationClass()->getTableFields() as $field) { 61 | $fieldName = "{$fullName}{$field}"; 62 | if(isset($row->{$fieldName})) { 63 | $attributes[$field] = $row->{$fieldName}; 64 | } 65 | } 66 | $entity->setAttributes($attributes); 67 | if(!$entity->exists()) continue; 68 | //$entity->resetStoredFields(); 69 | 70 | $relationName = $relation->getSimpleName(); 71 | switch($relation->getType()) { 72 | case RelationDef::HasOne: 73 | $current->{$relationName} = $entity; 74 | break; 75 | case RelationDef::HasMany: 76 | $relationName = plural($relationName); 77 | if(!isset($current->{$relationName})) { 78 | $current->{$relationName} = clone $entity; 79 | $current->{$relationName}->all = []; 80 | } 81 | $current->{$relationName}->all[] = $entity; 82 | break; 83 | } 84 | 85 | } 86 | 87 | } 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /Examples/Controllers/Crud.php: -------------------------------------------------------------------------------- 1 | cache->clean(); 16 | Setup::run(); 17 | } 18 | 19 | public function create() { 20 | $this->printBefore(); 21 | 22 | // Create new user 23 | $user = new User(); 24 | $user->name = "New user"; 25 | $user->save(); 26 | 27 | $this->printAfter(); 28 | 29 | $this->response->setJSON(Data::getStore()); 30 | $this->response->send(); 31 | } 32 | 33 | public function read() { 34 | // Find all users 35 | $users = (new UserModel())->findAll(); 36 | Data::set('users', $users->all); 37 | 38 | $this->response->setJSON(Data::getStore()); 39 | $this->response->send(); 40 | } 41 | 42 | public function update() { 43 | $this->printBefore(); 44 | 45 | // Find and Update user 46 | /** @var User $user */ 47 | $user = (new UserModel())->find(2); 48 | $user->name = "Updated user (2)"; 49 | $user->save(); 50 | 51 | $this->printAfter(); 52 | 53 | $this->response->setJSON(Data::getStore()); 54 | $this->response->send(); 55 | } 56 | 57 | public function delete() { 58 | $this->printBefore(); 59 | 60 | // Find and Delete user 61 | $user = (new UserModel())->find(2); 62 | $user->delete(); 63 | 64 | $this->printAfter(); 65 | 66 | $this->response->setJSON(Data::getStore()); 67 | $this->response->send(); 68 | } 69 | 70 | public function save_relations() { 71 | $this->printBefore(); 72 | 73 | // Insert user and relations 74 | $user = new User(); 75 | $user->name = 'With all roles'; 76 | $user->save(); 77 | $roleModel = new RoleModel(); 78 | $user->save($roleModel->find(1)); 79 | $user->save($roleModel->find(2)); 80 | $colorModel = new ColorModel(); 81 | $user->save($colorModel->find(1)); 82 | 83 | $this->printAfter(); 84 | 85 | $this->response->setJSON(Data::getStore()); 86 | $this->response->send(); 87 | } 88 | 89 | public function delete_relations() { 90 | //$this->printBefore(); 91 | 92 | // Find and delete relations 93 | $user = (new UserModel())->find(1); 94 | $roleModel = new RoleModel(); 95 | $user->delete($roleModel->find(1)); 96 | $colorModel = new ColorModel(); 97 | $user->delete($colorModel->find(1)); 98 | Data::lastQuery(); 99 | 100 | $this->printAfter(); 101 | 102 | $this->response->setJSON(Data::getStore()); 103 | $this->response->send(); 104 | } 105 | 106 | public function select_update() { 107 | //$this->printBefore(); 108 | 109 | $userModel = new UserModel(); 110 | $user = $userModel->find(1); 111 | $user->name = null; 112 | $userModel->save($user); 113 | Data::lastQuery(); 114 | 115 | $this->printAfter(); 116 | 117 | $this->response->setJSON(Data::getStore()); 118 | $this->response->send(); 119 | } 120 | 121 | public function group_by() { 122 | $users = (new UserModel()) 123 | ->groupByRelated(RoleModel::class, 'name') 124 | ->find(); 125 | Data::set('by role', $users->allToArray()); 126 | Data::lastQuery(); 127 | $users = (new UserModel()) 128 | ->groupByRelated(ColorModel::class, 'name') 129 | ->find(); 130 | Data::set('by color', $users->allToArray()); 131 | Data::lastQuery(); 132 | 133 | $this->response->setJSON(Data::getStore()); 134 | $this->response->send(); 135 | } 136 | 137 | public function having() { 138 | $users = (new UserModel()) 139 | ->includeRelated(RoleModel::class) 140 | ->havingRelated(RoleModel::class, 'name', 'admin') 141 | ->find(); 142 | Data::set('having admins', $users->allToArray()); 143 | Data::lastQuery(); 144 | $users = (new UserModel()) 145 | ->includeRelated(ColorModel::class) 146 | ->havingRelated(ColorModel::class, 'name', 'green') 147 | ->find(); 148 | Data::set('having green', $users->allToArray()); 149 | Data::lastQuery(); 150 | 151 | $this->response->setJSON(Data::getStore()); 152 | $this->response->send(); 153 | } 154 | 155 | public function order_by() { 156 | $users = (new UserModel()) 157 | ->orderByRelated(ColorModel::class, 'name') 158 | ->find(); 159 | Data::set('order by color asc', $users->allToArray()); 160 | Data::lastQuery(); 161 | $users = (new UserModel()) 162 | ->orderByRelated(ColorModel::class, 'name', 'desc') 163 | ->find(); 164 | Data::set('order by color desc', $users->allToArray()); 165 | Data::lastQuery(); 166 | 167 | $this->response->setJSON(Data::getStore()); 168 | $this->response->send(); 169 | } 170 | 171 | 172 | 173 | 174 | 175 | 176 | private function printBefore() { 177 | $users = (new UserModel()) 178 | ->includeRelated(RoleModel::class) 179 | ->find(); 180 | Data::set('users_before', $users->allToArray()); 181 | } 182 | 183 | private function printAfter() { 184 | $users = (new UserModel()) 185 | ->includeRelated(RoleModel::class) 186 | ->find(); 187 | Data::set('users_after', $users->allToArray()); 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /Examples/Controllers/Like.php: -------------------------------------------------------------------------------- 1 | cache->clean(); 15 | Setup::run(); 16 | } 17 | 18 | public function simple() { 19 | $model = new UserModel(); 20 | $user = $model 21 | ->like('name', 'green') 22 | ->find(); 23 | Data::set('user', $user->allToArray()); 24 | Data::lastQuery(); 25 | 26 | $this->response->setJSON(Data::getStore()); 27 | $this->response->send(); 28 | } 29 | 30 | public function not() { 31 | $model = new UserModel(); 32 | $user = $model 33 | ->notLike('name', 'green') 34 | ->find(); 35 | Data::set('user', $user->allToArray()); 36 | Data::lastQuery(); 37 | 38 | $this->response->setJSON(Data::getStore()); 39 | $this->response->send(); 40 | } 41 | 42 | public function or() { 43 | $model = new UserModel(); 44 | $user = $model 45 | ->like('name', 'red') 46 | ->orLike('name', 'green') 47 | ->find(); 48 | Data::set('user', $user->allToArray()); 49 | Data::lastQuery(); 50 | 51 | $this->response->setJSON(Data::getStore()); 52 | $this->response->send(); 53 | } 54 | 55 | public function or_not() { 56 | $model = new UserModel(); 57 | $user = $model 58 | ->like('name', 'red') 59 | ->orNotLike('name', 'green') 60 | ->find(); 61 | Data::set('user', $user->allToArray()); 62 | Data::lastQuery(); 63 | 64 | $this->response->setJSON(Data::getStore()); 65 | $this->response->send(); 66 | } 67 | 68 | public function related() { 69 | $model = new UserModel(); 70 | $user = $model 71 | ->likeRelated(ColorModel::class, 'name', 'green') 72 | ->find(); 73 | Data::set('user', $user->allToArray()); 74 | Data::lastQuery(); 75 | 76 | $this->response->setJSON(Data::getStore()); 77 | $this->response->send(); 78 | } 79 | 80 | public function not_related() { 81 | $model = new UserModel(); 82 | $user = $model 83 | ->notLikeRelated(ColorModel::class, 'name', 'green') 84 | ->find(); 85 | Data::set('user', $user->allToArray()); 86 | Data::lastQuery(); 87 | 88 | $this->response->setJSON(Data::getStore()); 89 | $this->response->send(); 90 | } 91 | 92 | public function or_related() { 93 | $model = new UserModel(); 94 | $user = $model 95 | ->likeRelated(ColorModel::class, 'name', 'red') 96 | ->orLikeRelated(ColorModel::class, 'name', 'green') 97 | ->find(); 98 | Data::set('user', $user->allToArray()); 99 | Data::lastQuery(); 100 | 101 | $this->response->setJSON(Data::getStore()); 102 | $this->response->send(); 103 | } 104 | 105 | public function or_not_related() { 106 | $model = new UserModel(); 107 | $user = $model 108 | ->likeRelated(ColorModel::class, 'name', 'red') 109 | ->orNotLikeRelated(ColorModel::class, 'name', 'green') 110 | ->find(); 111 | Data::set('user', $user->allToArray()); 112 | Data::lastQuery(); 113 | 114 | $this->response->setJSON(Data::getStore()); 115 | $this->response->send(); 116 | } 117 | 118 | } -------------------------------------------------------------------------------- /Examples/Controllers/Related.php: -------------------------------------------------------------------------------- 1 | cache->clean(); 17 | Setup::run(); 18 | } 19 | 20 | public function simple() { 21 | $model = new UserModel(); 22 | $model 23 | ->includeRelated(UserDetailModel::class) 24 | ->whereRelated(RoleModel::class, 'name', 'employee'); 25 | $users = $model->find(); 26 | Data::lastQuery(); 27 | 28 | foreach($users as $user) { 29 | $user->color->find(); 30 | $user->roles->find(); 31 | } 32 | 33 | Data::set('user', $users->allToArray()); 34 | 35 | $this->response->setJSON(Data::getStore()); 36 | $this->response->send(); 37 | } 38 | 39 | public function in() { 40 | $model = new UserModel(); 41 | $user = $model 42 | ->whereInRelated(RoleModel::class, 'name', ['admin', 'employee']) 43 | ->find(); 44 | Data::set('user', $user->allToArray()); 45 | Data::lastQuery(); 46 | 47 | $this->response->setJSON(Data::getStore()); 48 | $this->response->send(); 49 | } 50 | 51 | public function not_in() { 52 | $model = new UserModel(); 53 | $user = $model 54 | ->whereNotInRelated(RoleModel::class, 'name', ['admin', 'employee']) 55 | ->find(); 56 | Data::set('user', $user->allToArray()); 57 | Data::lastQuery(); 58 | 59 | $this->response->setJSON(Data::getStore()); 60 | $this->response->send(); 61 | } 62 | 63 | public function or() { 64 | $model = new UserModel(); 65 | $user = $model 66 | ->whereRelated(ColorModel::class, 'name', 'green') 67 | ->orWhereRelated(ColorModel::class, 'name', 'blue') 68 | ->find(); 69 | Data::set('user', $user->allToArray()); 70 | Data::lastQuery(); 71 | 72 | $this->response->setJSON(Data::getStore()); 73 | $this->response->send(); 74 | } 75 | 76 | public function or_in() { 77 | $model = new UserModel(); 78 | $user = $model 79 | ->whereRelated(ColorModel::class, 'name', 'green') 80 | ->orWhereInRelated(ColorModel::class, 'name', ['blue', 'red']) 81 | ->find(); 82 | Data::set('user', $user->allToArray()); 83 | Data::lastQuery(); 84 | 85 | $this->response->setJSON(Data::getStore()); 86 | $this->response->send(); 87 | } 88 | 89 | public function or_not_in() { 90 | $model = new UserModel(); 91 | $user = $model 92 | ->whereRelated(ColorModel::class, 'name', 'green') 93 | ->orWhereNotInRelated(ColorModel::class, 'name', ['blue']) 94 | ->find(); 95 | Data::set('user', $user->allToArray()); 96 | Data::lastQuery(); 97 | 98 | $this->response->setJSON(Data::getStore()); 99 | $this->response->send(); 100 | } 101 | 102 | public function between() { 103 | $model = new UserModel(); 104 | $user = $model 105 | ->whereBetweenRelated(ColorModel::class, 'id', 2, 3) 106 | ->find(); 107 | Data::set('user', $user->allToArray()); 108 | Data::lastQuery(); 109 | 110 | $this->response->setJSON(Data::getStore()); 111 | $this->response->send(); 112 | } 113 | 114 | public function not_between() { 115 | $model = new UserModel(); 116 | $user = $model 117 | ->whereNotBetweenRelated(ColorModel::class, 'id', 2, 3) 118 | ->find(); 119 | Data::set('user', $user->allToArray()); 120 | Data::lastQuery(); 121 | 122 | $this->response->setJSON(Data::getStore()); 123 | $this->response->send(); 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /Examples/Controllers/SubQuery.php: -------------------------------------------------------------------------------- 1 | cache->clean(); 16 | Setup::run(); 17 | } 18 | 19 | public function select() { 20 | $subQuery = (new RoleModel()) 21 | ->select('COUNT(*)', true, false) 22 | ->where('name', '${parent}.name', false); 23 | 24 | $model = new UserModel(); 25 | $user = $model 26 | ->select('*') 27 | ->selectSubQuery($subQuery, 'admin_roles') 28 | ->find(); 29 | Data::set('user', $user->allToArray()); 30 | Data::lastQuery(); 31 | 32 | $this->response->setJSON(Data::getStore()); 33 | $this->response->send(); 34 | } 35 | 36 | public function where() { 37 | $selectQuery = (new RoleModel()) 38 | ->select('COUNT(*)') 39 | ->whereRelated(UserModel::class, 'id', '${parent}.id', false); 40 | 41 | $whereQuery = (new RoleModel()) 42 | ->select('COUNT(*) as count') 43 | ->whereRelated(UserModel::class, 'id', '${parent}.id', true) 44 | ->having('count >', '1'); 45 | 46 | $model = new UserModel(); 47 | $user = $model 48 | //->selectSubQuery($selectQuery, 'roles_count') 49 | ->whereSubQuery($whereQuery) 50 | ->find(); 51 | //Data::set('user', $user->allToArray()); 52 | Data::lastQuery(); 53 | 54 | $this->response->setJSON(Data::getStore()); 55 | $this->response->send(); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /Examples/Controllers/Where.php: -------------------------------------------------------------------------------- 1 | cache->clean(); 14 | Setup::run(); 15 | } 16 | 17 | public function simple() { 18 | $model = new UserModel(); 19 | $user = $model 20 | ->where('id', 2) 21 | ->find(); 22 | Data::set('user', $user->toArray()); 23 | Data::lastQuery(); 24 | 25 | $this->response->setJSON(Data::getStore()); 26 | $this->response->send(); 27 | } 28 | 29 | public function in() { 30 | $model = new UserModel(); 31 | $user = $model 32 | ->whereIn('id', [2, 3]) 33 | ->find(); 34 | Data::set('user', $user->allToArray()); 35 | Data::lastQuery(); 36 | 37 | $this->response->setJSON(Data::getStore()); 38 | $this->response->send(); 39 | } 40 | 41 | public function not_in() { 42 | $model = new UserModel(); 43 | $user = $model 44 | ->whereNotIn('id', [2, 3]) 45 | ->find(); 46 | Data::set('user', $user->allToArray()); 47 | Data::lastQuery(); 48 | 49 | $this->response->setJSON(Data::getStore()); 50 | $this->response->send(); 51 | } 52 | 53 | public function or() { 54 | $model = new UserModel(); 55 | $user = $model 56 | ->where('id', 2) 57 | ->orWhere('id', 3) 58 | ->find(); 59 | Data::set('user', $user->allToArray()); 60 | Data::lastQuery(); 61 | 62 | $this->response->setJSON(Data::getStore()); 63 | $this->response->send(); 64 | } 65 | 66 | public function or_in() { 67 | $model = new UserModel(); 68 | $user = $model 69 | ->where('id', 2) 70 | ->orWhereIn('id', [3, 4]) 71 | ->find(); 72 | Data::set('user', $user->allToArray()); 73 | Data::lastQuery(); 74 | 75 | $this->response->setJSON(Data::getStore()); 76 | $this->response->send(); 77 | } 78 | 79 | public function or_not_in() { 80 | $model = new UserModel(); 81 | $user = $model 82 | ->where('id', 2) 83 | ->orWhereNotIn('id', [3, 4]) 84 | ->find(); 85 | Data::set('user', $user->allToArray()); 86 | Data::lastQuery(); 87 | 88 | $this->response->setJSON(Data::getStore()); 89 | $this->response->send(); 90 | } 91 | 92 | public function between() { 93 | $model = new UserModel(); 94 | $user = $model 95 | ->whereBetween('id', 2, 4) 96 | ->find(); 97 | Data::set('user', $user->allToArray()); 98 | Data::lastQuery(); 99 | 100 | $this->response->setJSON(Data::getStore()); 101 | $this->response->send(); 102 | } 103 | 104 | public function not_between() { 105 | $model = new UserModel(); 106 | $user = $model 107 | ->whereNotBetween('id', 2, 4) 108 | ->find(); 109 | Data::set('user', $user->allToArray()); 110 | Data::lastQuery(); 111 | 112 | $this->response->setJSON(Data::getStore()); 113 | $this->response->send(); 114 | } 115 | 116 | public function or_between() { 117 | $model = new UserModel(); 118 | $user = $model 119 | ->where('id', 1) 120 | ->orWhereBetween('id', 2, 4) 121 | ->find(); 122 | Data::set('user', $user->allToArray()); 123 | Data::lastQuery(); 124 | 125 | $this->response->setJSON(Data::getStore()); 126 | $this->response->send(); 127 | } 128 | 129 | public function or_not_between() { 130 | $model = new UserModel(); 131 | $user = $model 132 | ->where('id', 1) 133 | ->orWhereNotBetween('id', 2, 4) 134 | ->find(); 135 | Data::set('user', $user->allToArray()); 136 | Data::lastQuery(); 137 | 138 | $this->response->setJSON(Data::getStore()); 139 | $this->response->send(); 140 | } 141 | 142 | } -------------------------------------------------------------------------------- /Examples/Entities/Color.php: -------------------------------------------------------------------------------- 1 | name = 'admin'; 19 | $admin->save(); 20 | $employee = new Role(); 21 | $employee->name = 'employee'; 22 | $employee->save(); 23 | 24 | $green = new Color(); 25 | $green->name = 'green'; 26 | $green->save(); 27 | $blue = new Color(); 28 | $blue->name = 'blue'; 29 | $blue->save(); 30 | $red = new Color(); 31 | $red->name = 'red'; 32 | $red->save(); 33 | 34 | $detail1 = new UserDetail(); 35 | $detail1->address = "User 1 street"; 36 | $detail1->save(); 37 | $detail2 = new UserDetail(); 38 | $detail2->address = "User 2 street"; 39 | $detail2->save(); 40 | $detail3 = new UserDetail(); 41 | $detail3->address = "User 3 street"; 42 | $detail3->save(); 43 | $detail4 = new UserDetail(); 44 | $detail4->address = "User 4 street"; 45 | $detail4->save(); 46 | $detail5 = new UserDetail(); 47 | $detail5->address = "User 5 street"; 48 | $detail5->save(); 49 | $detail6 = new UserDetail(); 50 | $detail6->address = "User 6 street"; 51 | $detail6->save(); 52 | 53 | $user = new User(); 54 | $user->name = "Green admin"; 55 | $user->save(); 56 | $user->save($green); 57 | $user->save($admin); 58 | $user->save($detail1); 59 | $detail1->save($user); 60 | $user = new User(); 61 | $user->name = "Blue admin"; 62 | $user->save(); 63 | $user->save($blue); 64 | $user->save($admin); 65 | $user->save($detail2); 66 | $user = new User(); 67 | $user->name = "Red admin"; 68 | $user->save(); 69 | $user->save($red); 70 | $user->save($admin); 71 | $user->save($detail3); 72 | $user = new User(); 73 | $user->name = "Green employee"; 74 | $user->save(); 75 | $user->save($green); 76 | $user->save($employee); 77 | $user->save($detail4); 78 | $user = new User(); 79 | $user->name = "Blue employee"; 80 | $user->save(); 81 | $user->save($blue); 82 | $user->save($employee); 83 | $user->save($detail5); 84 | $user = new User(); 85 | $user->name = "Red employee"; 86 | $user->save(); 87 | $user->save($red); 88 | $user->save($employee); 89 | $user->save($detail6); 90 | } 91 | 92 | private static function addTables() { 93 | $forge = Database::forge(); 94 | $forge->dropTable('users', true); 95 | $forge->dropTable('roles', true); 96 | $forge->dropTable('roles_users', true); 97 | $forge->dropTable('colors', true); 98 | $forge->dropTable('user_details', true); 99 | 100 | $forge->addField([ 101 | 'id' => [ 102 | 'type' => 'INT', 103 | 'unsigned' => true, 104 | 'auto_increment' => true, 105 | ], 106 | 'name' => [ 107 | 'type' => 'VARCHAR', 108 | 'constraint' => '255', 109 | ], 110 | 'color_id' => [ 111 | 'type' => 'INT', 112 | 'unsigned' => true 113 | ], 114 | 'user_detail_id' => [ 115 | 'type' => 'INT', 116 | 'unsigned' => true 117 | ] 118 | ]); 119 | $forge->addPrimaryKey('id'); 120 | $forge->createTable('users'); 121 | 122 | 123 | $forge->addField([ 124 | 'id' => [ 125 | 'type' => 'INT', 126 | 'unsigned' => true, 127 | 'auto_increment' => true, 128 | ], 129 | 'name' => [ 130 | 'type' => 'VARCHAR', 131 | 'constraint' => '255', 132 | ] 133 | ]); 134 | $forge->addPrimaryKey('id'); 135 | $forge->createTable('roles'); 136 | 137 | 138 | $forge->addField([ 139 | 'id' => [ 140 | 'type' => 'INT', 141 | 'unsigned' => true, 142 | 'auto_increment' => true, 143 | ], 144 | 'role_id' => [ 145 | 'type' => 'INT', 146 | 'unsigned' => true, 147 | ], 148 | 'user_id' => [ 149 | 'type' => 'INT', 150 | 'unsigned' => true, 151 | ] 152 | ]); 153 | $forge->addPrimaryKey('id'); 154 | $forge->createTable('roles_users'); 155 | 156 | 157 | $forge->addField([ 158 | 'id' => [ 159 | 'type' => 'INT', 160 | 'unsigned' => true, 161 | 'auto_increment' => true, 162 | ], 163 | 'name' => [ 164 | 'type' => 'VARCHAR', 165 | 'constraint' => '255', 166 | ] 167 | ]); 168 | $forge->addPrimaryKey('id'); 169 | $forge->createTable('colors'); 170 | 171 | 172 | $forge->addField([ 173 | 'id' => [ 174 | 'type' => 'INT', 175 | 'unsigned' => true, 176 | 'auto_increment' => true, 177 | ], 178 | 'address' => [ 179 | 'type' => 'VARCHAR', 180 | 'constraint' => '255', 181 | ] 182 | ]); 183 | $forge->addPrimaryKey('id'); 184 | $forge->createTable('user_details'); 185 | } 186 | 187 | } -------------------------------------------------------------------------------- /Extensions/Database/BaseBuilder.php: -------------------------------------------------------------------------------- 1 | bindsKeyCount; 7 | } 8 | 9 | /** 10 | * @param mixed $key 11 | * @param null $value 12 | * @param bool $escape 13 | * @return \CodeIgniter\Database\BaseBuilder|BaseBuilder 14 | */ 15 | public function where($key, $value = null, bool $escape = null) { 16 | return parent::where($key, $value, $escape); 17 | } 18 | 19 | /** 20 | * @param string $sql 21 | * @param array $bindKeyCount 22 | * @param array $binds 23 | * @return string 24 | */ 25 | public function bindMerging($sql, $bindKeyCount, $binds) { 26 | // Ensure my keys are part of the bindsKeyCount 27 | // If not, CI4 will overwrite values https://github.com/codeigniter4/CodeIgniter4/issues/7049 28 | foreach ($binds as $key => $value) { 29 | if (!isset($bindKeyCount[$key])) { 30 | $bindKeyCount[$key] = 1; 31 | } 32 | } 33 | 34 | $this->bindsKeyCount = array_merge($bindKeyCount, $this->bindsKeyCount); 35 | foreach($binds as $key => [$value, $escape]) { 36 | $newKey = $this->setBind($key, $value, $escape); 37 | $sql = str_replace(":$key:", ":$newKey:", $sql); 38 | } 39 | return $sql; 40 | } 41 | 42 | /** 43 | * @param string $search 44 | * @param string $replace 45 | * @return string 46 | */ 47 | public function bindReplace($search, $replace) { 48 | foreach($this->getBinds() as $key => [$value, $escape]) { 49 | $this->binds[$key] = [str_replace($search, $replace, $value), $escape]; 50 | } 51 | } 52 | 53 | public function compileSelect_(): string { 54 | return parent::compileSelect(); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /Extensions/Database/MySQLi/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace OrmExtension\Extensions\Database\MySQLi; 13 | 14 | use OrmExtension\Extensions\Database\BaseBuilder; 15 | 16 | /** 17 | * Builder for MySQLi 18 | */ 19 | class Builder extends BaseBuilder 20 | { 21 | /** 22 | * Identifier escape character 23 | * 24 | * @var string 25 | */ 26 | protected $escapeChar = '`'; 27 | 28 | /** 29 | * Specifies which sql statements 30 | * support the ignore option. 31 | * 32 | * @var array 33 | */ 34 | protected $supportedIgnoreStatements = [ 35 | 'update' => 'IGNORE', 36 | 'insert' => 'IGNORE', 37 | 'delete' => 'IGNORE', 38 | ]; 39 | 40 | /** 41 | * FROM tables 42 | * 43 | * Groups tables in FROM clauses if needed, so there is no confusion 44 | * about operator precedence. 45 | * 46 | * Note: This is only used (and overridden) by MySQL. 47 | */ 48 | protected function _fromTables(): string 49 | { 50 | if (! empty($this->QBJoin) && count($this->QBFrom) > 1) { 51 | return '(' . implode(', ', $this->QBFrom) . ')'; 52 | } 53 | 54 | return implode(', ', $this->QBFrom); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Extensions/Database/MySQLi/Connection.php: -------------------------------------------------------------------------------- 1 | $value) 41 | { 42 | $this->__set($key, $value); 43 | } 44 | 45 | // Fill relations 46 | if ($data) { 47 | 48 | $relations = ModelDefinitionCache::getRelations($this->getSimpleName()); 49 | /** @var RelationDef[] $relationFields */ 50 | $relationFields = []; 51 | foreach ($relations as $relation) { 52 | $relationFields[$relation->getSimpleName()] = $relation; 53 | } 54 | 55 | $fields = $this->getTableFields(); 56 | foreach ($data as $key => $value) { 57 | $method = 'set' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key))); 58 | 59 | if (method_exists($this, $method)) { 60 | $this->$method($value); 61 | } else { // Does not require property to exist, properties are managed by OrmExtension from database columns 62 | if (in_array($key, $fields)) { // Does require field to exists 63 | $this->$key = $value; 64 | } else if (isset($relationFields[singular($key)])) { // Auto-populate relation 65 | $relation = $relationFields[singular($key)]; 66 | switch ($relation->getType()) { 67 | case RelationDef::HasOne: 68 | $entityName = $relation->getEntityName(); 69 | $this->$key = new $entityName($value); 70 | break; 71 | case RelationDef::HasMany: 72 | $entityName = $relation->getEntityName(); 73 | $this->{$key} = new $entityName(); 74 | /** @var Entity $relationMany */ 75 | $relationMany = $this->{$key}; 76 | foreach ($value as $v) { 77 | $relationMany->add(new $entityName($v)); 78 | } 79 | break; 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | private function getSimpleName() { 90 | return substr(strrchr(get_class($this), '\\'), 1); 91 | } 92 | 93 | private $_model; 94 | 95 | /** 96 | * @return Model|QueryBuilderInterface 97 | */ 98 | public function _getModel(): Model { 99 | if (!$this->_model) { 100 | foreach (OrmExtension::$modelNamespace as $modelNamespace) { 101 | $name = $modelNamespace . $this->getSimpleName() . 'Model'; 102 | if (class_exists($name)) { 103 | $this->_model = new $name(); 104 | break; 105 | } 106 | } 107 | } 108 | return $this->_model; 109 | } 110 | 111 | /** 112 | * Override System/Entity, cause of error "Undefined variable: result" 113 | * @param string $key 114 | * @return mixed|null 115 | */ 116 | public function __get(string $key) { 117 | helper('inflector'); 118 | $result = parent::__get($key); 119 | 120 | if (is_null($result) && $key != $this->_getModel()->getPrimaryKey()) { 121 | // Check for relation 122 | foreach ($this->_getModel()->getRelations() as $relation) { 123 | if ($relation->getSimpleName() == singular($key)) { 124 | $className = $relation->getEntityName(); 125 | $this->{$key} = new $className(); 126 | /** @var Entity $entity */ 127 | $entity = $this->attributes[$key]; 128 | $entityModel = $entity->_getModel(); 129 | 130 | // Data::debug(get_class($this), $relation->getName(), $relation->getJoinSelfAs(), $this->{$relation->getJoinOtherAs()}); 131 | // $entity->_getModel()->whereRelated($relation->getOtherField(), $this->_getModel()->getPrimaryKey(), $this->{$this->_getModel()->getPrimaryKey()}); 132 | // $entity->_getModel()->whereRelated($relation->getOtherField(), $relation->getJoinSelfAs(), $this->{$this->_getModel()->getPrimaryKey()}); 133 | 134 | [$lastJoinTable, $lastJoinModel] = $entityModel->handleWhereRelated($relation->getOtherField()); 135 | 136 | $field = null; 137 | $value = null; 138 | $relationShipTableFields = $lastJoinModel->getTableFields(); 139 | 140 | $joinSelfAsGuess = $relation->getJoinSelfAsGuess(); 141 | if (in_array($joinSelfAsGuess[0], $relationShipTableFields)) { 142 | $field = $joinSelfAsGuess[0]; 143 | $value = $this->{$field}; 144 | } else if (in_array($joinSelfAsGuess[1], $relationShipTableFields)) { 145 | $field = $joinSelfAsGuess[1]; 146 | $value = $this->{$field}; 147 | } else { 148 | $joinOtherAsGuess = $relation->getJoinOtherAsGuess(); 149 | if (in_array($joinOtherAsGuess[0], $relationShipTableFields)) { 150 | $value = $this->{$joinOtherAsGuess[0]}; 151 | } else if (in_array($joinOtherAsGuess[1], $relationShipTableFields)) { 152 | $value = $this->{$joinOtherAsGuess[1]}; 153 | } 154 | } 155 | 156 | if (is_null($field) || is_null($value)) { 157 | // Undefined relationship table. We have to relay on relationship definitions 158 | $field = $relation->getJoinSelfAs(); 159 | $value = $this->{$relation->getJoinOtherAs()}; 160 | } 161 | 162 | $entity 163 | ->_getModel() 164 | ->whereRelated( 165 | $relation->getOtherField(), 166 | $field, 167 | $value 168 | ); 169 | $result = $entity; 170 | break; 171 | } 172 | } 173 | } 174 | 175 | return $result; 176 | } 177 | 178 | /** 179 | * @param string $key 180 | * @param mixed $value 181 | * @return \CodeIgniter\Entity\Entity|Entity 182 | */ 183 | public function __set(string $key, $value = null) { 184 | parent::__set($key, $value); 185 | return $this; 186 | } 187 | 188 | /** 189 | * @return Entity 190 | */ 191 | public function first() { 192 | return isset($this->all) ? reset($this->all) : $this; 193 | } 194 | 195 | public function setAttributes(array $data) { 196 | // Type casting 197 | $fieldData = ModelDefinitionCache::getFieldData($this->getSimpleName()); 198 | $fieldName2Type = []; 199 | foreach ($fieldData as $field) $fieldName2Type[$field->name] = $field->type; 200 | 201 | foreach ($data as $field => $value) { 202 | if (isset($fieldName2Type[$field])) { 203 | switch ($fieldName2Type[$field]) { 204 | case 'int': 205 | $data[$field] = is_null($value) ? null : (int)$value; 206 | break; 207 | case 'float': 208 | case 'double': 209 | case 'decimal': 210 | $data[$field] = is_null($value) ? null : (double)$value; 211 | break; 212 | case 'tinyint': 213 | $data[$field] = (bool)$value; 214 | break; 215 | case 'datetime': 216 | $data[$field] = $value ? date('Y-m-d H:i:s', strtotime($value)) : null; 217 | break; 218 | default: 219 | $data[$field] = $value; 220 | } 221 | } 222 | } 223 | 224 | return parent::setAttributes($data); 225 | } 226 | 227 | public function getOriginal($key = null) { 228 | if ($key) { 229 | return $this->original[$key]; 230 | } else { 231 | return $this->original; 232 | } 233 | } 234 | 235 | public function hasChanged(string $key = null, $checkRelations = false): bool { 236 | if ($key === null && $checkRelations == false) { 237 | // CI4 will check original against attributes. Attributes holds everything, including relations 238 | // Remove relations before checking 239 | $tableFields = []; 240 | foreach ($this->getTableFields() as $tableField) { 241 | $tableFields[$tableField] = $tableField; 242 | } 243 | $original = array_intersect_key($this->original, $tableFields); 244 | $attributes = array_intersect_key($this->attributes, $tableFields); 245 | return $original !== $attributes; 246 | } else 247 | return parent::hasChanged($key); 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /Extensions/Model.php: -------------------------------------------------------------------------------- 1 | setCodeIgniterModelStuff(); 41 | parent::__construct($db, $validation); 42 | } 43 | 44 | // 45 | 46 | use QueryBuilder; 47 | 48 | public $hasOne = []; 49 | public $hasMany = []; 50 | 51 | protected function _getModel(): Model { 52 | return $this; 53 | } 54 | 55 | protected $builder; 56 | protected function _getBuilder(): BaseBuilder { 57 | return $this->builder; 58 | } 59 | 60 | public function _setBuilder(BaseBuilder $builder) { 61 | $this->builder = $builder; 62 | } 63 | 64 | private function appendTable(&$key) { 65 | $key = "{$this->getTableName()}.{$key}"; 66 | } 67 | 68 | // 69 | 70 | private $selecting = false; 71 | public function isSelecting() { 72 | return $this->selecting; 73 | } 74 | public function setSelecting($selecting) { 75 | $this->selecting = $selecting; 76 | } 77 | 78 | /** 79 | * @param string|array $select 80 | * @param boolean $escape 81 | * @param boolean $appendTable 82 | * @return BaseBuilder|Model 83 | */ 84 | public function select($select = '*', bool $escape = null, $appendTable = true): Model { 85 | $this->selecting = true; 86 | if($appendTable) { 87 | if(strpos($select, '.') === false) { 88 | $selects = explode(',', $select); 89 | foreach($selects as &$select) { 90 | $select = trim($select); 91 | $this->appendTable($select); 92 | } 93 | $select = implode(', ', $selects); 94 | } 95 | } 96 | return parent::select($select, $escape); 97 | } 98 | 99 | // 100 | 101 | // 102 | 103 | /** 104 | * @param mixed $key 105 | * @param mixed $value 106 | * @param boolean $escape 107 | * @param bool $appendTable 108 | * @return BaseBuilder|Model 109 | */ 110 | public function where($key, $value = null, bool $escape = null, $appendTable = true) { 111 | if($appendTable) $this->appendTable($key); 112 | return parent::where($key, $value, $escape); 113 | } 114 | 115 | /** 116 | * @param $key 117 | * @param int $min 118 | * @param int $max 119 | * @param null $escape 120 | * @param bool $appendTable 121 | * @return BaseBuilder|Model 122 | */ 123 | public function whereBetween($key, $min = 0, $max = 0, $escape = null, $appendTable = true) { 124 | if($appendTable) $this->appendTable($key); 125 | return parent::where("`$key` BETWEEN \"{$min}\" AND \"{$max}\"", null, $escape); 126 | } 127 | 128 | /** 129 | * @param $key 130 | * @param int $min 131 | * @param int $max 132 | * @param bool $escape 133 | * @param bool $appendTable 134 | * @return BaseBuilder|Model 135 | */ 136 | public function whereNotBetween($key, $min = 0, $max = 0, $escape = false, $appendTable = true) { 137 | if($appendTable) $this->appendTable($key); 138 | return parent::where("$key NOT BETWEEN \"{$min}\" AND \"{$max}\"", null, $escape); 139 | } 140 | 141 | /** 142 | * @param string $key 143 | * @param mixed $values 144 | * @param boolean $escape 145 | * @param boolean $appendTable 146 | * @return BaseBuilder|Model 147 | */ 148 | public function whereIn(string $key = null, $values = null, bool $escape = null, $appendTable = true) { 149 | if($appendTable) $this->appendTable($key); 150 | if($values instanceof Entity) { 151 | $ids = []; 152 | foreach($values as $value) $ids[] = $value->{$this->getPrimaryKey()}; 153 | $values = $ids; 154 | } 155 | if(is_string($values)) $values = [$values]; 156 | return parent::whereIn($key, $values, $escape); 157 | } 158 | 159 | /** 160 | * @param string $key 161 | * @param mixed $values 162 | * @param boolean $escape 163 | * @param boolean $appendTable 164 | * @return BaseBuilder|Model 165 | */ 166 | public function whereNotIn(string $key = null, $values = null, bool $escape = null, $appendTable = true) { 167 | if($appendTable) $this->appendTable($key); 168 | if($values instanceof Entity) { 169 | $ids = []; 170 | foreach($values as $value) $ids[] = $value->{$this->getPrimaryKey()}; 171 | $values = $ids; 172 | } 173 | return parent::whereNotIn($key, $values, $escape); 174 | } 175 | 176 | // 177 | 178 | // 179 | 180 | /** 181 | * @param mixed $key 182 | * @param mixed $value 183 | * @param boolean $escape 184 | * @param boolean $appendTable 185 | * @return BaseBuilder|Model 186 | */ 187 | public function orWhere($key, $value = null, bool $escape = null, $appendTable = true) { 188 | if($appendTable) $this->appendTable($key); 189 | return parent::orWhere($key, $value, $escape); 190 | } 191 | 192 | /** 193 | * @param $key 194 | * @param int $min 195 | * @param int $max 196 | * @param null $escape 197 | * @param bool $appendTable 198 | * @return BaseBuilder|Model 199 | */ 200 | public function orWhereBetween($key, $min = 0, $max = 0, $escape = null, $appendTable = true) { 201 | if($appendTable) $this->appendTable($key); 202 | return parent::orWhere("`$key` BETWEEN {$min} AND {$max}", null, $escape); 203 | } 204 | 205 | /** 206 | * @param $key 207 | * @param int $min 208 | * @param int $max 209 | * @param bool $escape 210 | * @param bool $appendTable 211 | * @return BaseBuilder|Model 212 | */ 213 | public function orWhereNotBetween($key, $min = 0, $max = 0, $escape = false, $appendTable = true) { 214 | if($appendTable) $this->appendTable($key); 215 | return parent::orWhere("$key NOT BETWEEN {$min} AND {$max}", null, $escape); 216 | } 217 | 218 | /** 219 | * @param string $key 220 | * @param array $values 221 | * @param boolean $escape 222 | * @param boolean $appendTable 223 | * @return BaseBuilder|Model 224 | */ 225 | public function orWhereIn(string $key = null, array $values = null, bool $escape = null, $appendTable = true) { 226 | if($appendTable) $this->appendTable($key); 227 | return parent::orWhereIn($key, $values, $escape); 228 | } 229 | 230 | /** 231 | * @param string $key 232 | * @param array $values 233 | * @param boolean $escape 234 | * @param boolean $appendTable 235 | * @return BaseBuilder|Model 236 | */ 237 | public function orWhereNotIn(string $key = null, array $values = null, bool $escape = null, $appendTable = true) { 238 | if($appendTable) $this->appendTable($key); 239 | return parent::orWhereNotIn($key, $values, $escape); 240 | } 241 | 242 | 243 | // 244 | 245 | // 246 | 247 | /** 248 | * @param mixed $field 249 | * @param string $match 250 | * @param string $side 251 | * @param boolean $escape 252 | * @param boolean $insensitiveSearch 253 | * @param bool $appendTable 254 | * @return BaseBuilder|Model 255 | */ 256 | public function like($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false, $appendTable = true) { 257 | if($appendTable) $this->appendTable($field); 258 | return parent::like($field, $match, $side, $escape, $insensitiveSearch); 259 | } 260 | 261 | /** 262 | * @param mixed $field 263 | * @param string $match 264 | * @param string $side 265 | * @param boolean $escape 266 | * @param boolean $insensitiveSearch 267 | * @param bool $appendTable 268 | * @return BaseBuilder|Model 269 | */ 270 | public function notLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false, $appendTable = true) { 271 | if($appendTable) $this->appendTable($field); 272 | return parent::notLike($field, $match, $side, $escape, $insensitiveSearch); 273 | } 274 | 275 | /** 276 | * @param mixed $field 277 | * @param string $match 278 | * @param string $side 279 | * @param boolean $escape 280 | * @param boolean $insensitiveSearch 281 | * @param bool $appendTable 282 | * @return BaseBuilder|Model 283 | */ 284 | public function orLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false, $appendTable = true) { 285 | if($appendTable) $this->appendTable($field); 286 | return parent::orLike($field, $match, $side, $escape, $insensitiveSearch); 287 | } 288 | 289 | /** 290 | * @param mixed $field 291 | * @param string $match 292 | * @param string $side 293 | * @param boolean $escape 294 | * @param boolean $insensitiveSearch 295 | * @param bool $appendTable 296 | * @return BaseBuilder|Model 297 | */ 298 | public function orNotLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false, $appendTable = true) { 299 | if($appendTable) $this->appendTable($field); 300 | return parent::orNotLike($field, $match, $side, $escape, $insensitiveSearch); 301 | } 302 | 303 | // 304 | 305 | // 306 | 307 | /** 308 | * @param string $by 309 | * @param boolean $escape 310 | * @param boolean $appendTable 311 | * @return BaseBuilder|Model 312 | */ 313 | public function groupBy($by, bool $escape = null, $appendTable = true) { 314 | if($appendTable) $this->appendTable($by); 315 | return parent::groupBy($by, $escape); 316 | } 317 | 318 | /** 319 | * @param string|array $key 320 | * @param mixed $value 321 | * @param boolean $escape 322 | * @param boolean $appendTable 323 | * @return BaseBuilder|Model 324 | */ 325 | public function having($key, $value = null, bool $escape = null, $appendTable = true) { 326 | if($appendTable) $this->appendTable($key); 327 | return parent::having($key, $value, $escape); 328 | } 329 | 330 | /** 331 | * @param string|array $key 332 | * @param mixed $value 333 | * @param boolean $escape 334 | * @param boolean $appendTable 335 | * @return BaseBuilder|Model 336 | */ 337 | public function orHaving($key, $value = null, bool $escape = null, $appendTable = true) { 338 | if($appendTable) $this->appendTable($key); 339 | return parent::orHaving($key, $value, $escape); 340 | } 341 | 342 | /** 343 | * @param string $orderBy 344 | * @param string $direction ASC, DESC, RANDOM or IS NULL 345 | * @param boolean $escape 346 | * @param boolean $appendTable 347 | * @return BaseBuilder|Model 348 | */ 349 | public function orderBy(string $orderBy, string $direction = '', bool $escape = null, $appendTable = true) { 350 | if($appendTable) $this->appendTable($orderBy); 351 | 352 | if(is_null($direction) || $direction == 'null') { 353 | return $this->orderByHack($orderBy, 'IS NULL', $escape); 354 | } else if(in_array($direction, ['null asc', 'null desc'])) { 355 | [$_, $nullDirection] = explode(' ', $direction); 356 | return $this->orderByHack($orderBy, "IS NULL $nullDirection", $escape); 357 | } 358 | 359 | return parent::orderBy($orderBy, $direction, $escape); 360 | } 361 | 362 | private function orderByHack($orderby, $direction = '', $escape = null) { 363 | $direction = strtoupper(trim($direction)); 364 | 365 | if(empty($orderby)) 366 | return $this; 367 | else if ($direction !== '') { 368 | $direction = in_array($direction, ['IS NULL', 'IS NULL DESC', 'IS NULL ASC'], true) ? ' ' . $direction : ''; 369 | } 370 | 371 | is_bool($escape) || $escape = $this->db->protectIdentifiers; 372 | 373 | if ($escape === false) 374 | { 375 | $qb_orderby[] = [ 376 | 'field' => $orderby, 377 | 'direction' => $direction, 378 | 'escape' => false, 379 | ]; 380 | } 381 | else 382 | { 383 | $qb_orderby = []; 384 | foreach (explode(',', $orderby) as $field) 385 | { 386 | $qb_orderby[] = ($direction === '' && 387 | preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE)) ? [ 388 | 'field' => ltrim(substr($field, 0, $match[0][1])), 389 | 'direction' => ' ' . $match[1][0], 390 | 'escape' => true, 391 | ] : [ 392 | 'field' => trim($field), 393 | 'direction' => $direction, 394 | 'escape' => true, 395 | ]; 396 | } 397 | } 398 | 399 | $this->builder()->QBOrderBy = array_merge($this->builder()->QBOrderBy, $qb_orderby); 400 | 401 | return $this; 402 | } 403 | 404 | // 405 | 406 | // 407 | 408 | /** 409 | * @param string $not 410 | * @param string $type 411 | * @return BaseBuilder|Model 412 | */ 413 | public function groupStart(string $not = '', string $type = 'AND ') { 414 | return parent::groupStart($not, $type); 415 | } 416 | 417 | /** 418 | * @return BaseBuilder|Model 419 | */ 420 | public function orGroupStart() { 421 | return parent::orGroupStart(); 422 | } 423 | 424 | /** 425 | * @return BaseBuilder|Model 426 | */ 427 | public function groupEnd() { 428 | return parent::groupEnd(); 429 | } 430 | 431 | // 432 | 433 | /** 434 | * @param null $id 435 | * @return array|object|null|Entity 436 | */ 437 | public function find($id = null) { 438 | $result = parent::find($id); 439 | // Clear 440 | $this->setSelecting(false); 441 | return $result; 442 | } 443 | 444 | 445 | // 446 | 447 | 448 | // 449 | 450 | use ResultBuilder; 451 | 452 | /** 453 | * Called after find 454 | * @param array $data 455 | * @return mixed 456 | */ 457 | protected function handleResult(array $data) { 458 | if(empty($data['data'])) { 459 | $data['data'] = new $this->returnType(); 460 | return $data; 461 | } 462 | $result = $data['data']; 463 | 464 | if($result instanceof Entity) { 465 | $result = [$result]; 466 | } 467 | 468 | $this->arrangeIncludedRelations($result); 469 | 470 | // Convert from array to single Entity 471 | if(is_array($result) && count($result) > 0) { 472 | $first = clone $result[0]; 473 | foreach($result as $item) { 474 | $first->add($item); 475 | } 476 | $result = $first; 477 | } else { 478 | $result = new $this->returnType(); 479 | } 480 | 481 | $data['data'] = $result; 482 | return $data; 483 | } 484 | 485 | // 486 | 487 | 488 | 489 | // 490 | 491 | /** @var Entity $entityToSave */ 492 | private $entityToSave; 493 | /** @var array $updatedData */ 494 | private $updatedData; 495 | 496 | /** 497 | * @param Entity $entity 498 | * @param bool $isNew 499 | * @return void 500 | */ 501 | protected function prepareSave($entity, $isNew) { 502 | $this->entityToSave = $entity; 503 | 504 | if($isNew && $this->useTimestamps && in_array($this->createdField, $this->getTableFields())) { 505 | if(empty($entity->{$this->createdField})) 506 | $entity->{$this->createdField} = $this->setDate(); 507 | } 508 | if(!$isNew && $this->useTimestamps && in_array($this->updatedField, $this->getTableFields())) { 509 | $entity->{$this->updatedField} = $this->setDate(); 510 | } 511 | } 512 | 513 | /** 514 | * @param bool $result 515 | * @param Entity $entity 516 | * @param bool $isNew 517 | * @return mixed 518 | */ 519 | protected function completeSave($result, $entity, $isNew) { 520 | if($result && empty($entity->{$this->getPrimaryKey()})) 521 | $entity->{$this->getPrimaryKey()} = $result; 522 | 523 | //$entity->resetStoredFields(); 524 | $entity->syncOriginal(); 525 | 526 | if($this instanceof OrmEventsInterface) { 527 | if($isNew) 528 | $this->postCreation($entity); 529 | else { 530 | // Clean up updateData, CI4 likes to put primaryKey in every update 531 | if(isset($this->updatedData[$this->getPrimaryKey()])) { 532 | $updatedPrimaryKey = $this->updatedData[$this->getPrimaryKey()]; 533 | if($updatedPrimaryKey['old'] == $updatedPrimaryKey['old']) 534 | unset($this->updatedData[$this->getPrimaryKey()]); 535 | } 536 | 537 | $this->postUpdate($entity, $this->updatedData); 538 | } 539 | } 540 | 541 | return $result; 542 | } 543 | 544 | /** 545 | * @param Entity $entity 546 | * @return bool 547 | * @throws \ReflectionException 548 | */ 549 | public function save($entity): bool { 550 | $isNew = empty($entity->{$this->getPrimaryKey()}) || is_null($entity->{$this->getPrimaryKey()}); 551 | $this->prepareSave($entity, $isNew); 552 | 553 | $result = $this->saveAndReturnId($entity); 554 | return $this->completeSave($result, $entity, $isNew); 555 | } 556 | 557 | /** 558 | * @param $data 559 | * @return int 560 | * @throws \ReflectionException 561 | */ 562 | private function saveAndReturnId($data) { 563 | if(empty($data)) return true; 564 | 565 | if(is_object($data) && isset($data->{$this->primaryKey})) { 566 | try { 567 | parent::update($data->{$this->primaryKey}, $data); 568 | } catch (DataException $e) { 569 | if ($e->getMessage() == 'There is no data to update.') { 570 | // Ignore empty update exceptions 571 | } 572 | } 573 | return $data->{$this->primaryKey}; 574 | } elseif (is_array($data) && !empty($data[$this->primaryKey])) { 575 | try { 576 | parent::update($data[$this->primaryKey], $data); 577 | } catch (DataException $e) { 578 | if ($e->getMessage() == 'There is no data to update.') { 579 | // Ignore empty update exceptions 580 | } 581 | } 582 | return $data[$this->primaryKey]; 583 | } else { 584 | return parent::insert($data, true); 585 | } 586 | } 587 | 588 | /** 589 | * @param Entity $entity 590 | * @param bool $returnID 591 | * @return bool|int|string|void 592 | * @throws \ReflectionException 593 | */ 594 | public function insert($entity = null, bool $returnID = true) { 595 | $this->prepareSave($entity, true); 596 | $result = parent::insert($entity, false); 597 | return $this->completeSave($result, $entity, true); 598 | } 599 | 600 | /** 601 | * Called before update 602 | * @param array $data 603 | * @return array 604 | */ 605 | public function modifyUpdateFields($data) { 606 | $this->updatedData = []; 607 | if($this->entityToSave instanceof Entity) { 608 | $fields = $data['data']; 609 | $original = $this->entityToSave->getOriginal(); 610 | foreach($fields as $field => $value) { 611 | $this->updatedData[$field] = [ 612 | 'old' => isset($original[$field]) ? $original[$field] : null, 613 | 'new' => $fields[$field] 614 | ]; 615 | } 616 | if(empty($fields)) { // Set the id field, CI dont like empty updates 617 | $fields[$this->getPrimaryKey()] = $original[$this->getPrimaryKey()]; 618 | } 619 | $data['data'] = $fields; 620 | } 621 | unset($this->entityToSave); 622 | return $data; 623 | } 624 | 625 | /** 626 | * Called before insert 627 | * @param array $data 628 | * @return array 629 | */ 630 | public function modifyInsertFields($data) { 631 | if($this->entityToSave instanceof Entity) { 632 | $fields = $data['data']; 633 | foreach($fields as $field => $value) { 634 | if(is_null($value)) 635 | unset($fields[$field]); 636 | } 637 | if(empty($fields)) { // Set the id field, CI dont like empty updates 638 | $fields[$this->getPrimaryKey()] = 0; 639 | } 640 | $data['data'] = $fields; 641 | } 642 | unset($this->entityToSave); 643 | return $data; 644 | } 645 | 646 | // 647 | 648 | 649 | // 650 | 651 | protected $table = null; 652 | protected $returnType = null; 653 | protected $entityName = null; 654 | 655 | private function setCodeIgniterModelStuff() { 656 | if (!isset($this->table)) { 657 | $this->table = $this->getTableName(); 658 | } 659 | if (!isset($this->entityName)) { 660 | $this->entityName = $this->getEntityName(); 661 | } 662 | 663 | if (!isset($this->returnType)) { 664 | foreach (OrmExtension::$entityNamespace as $entityNamespace) { 665 | $this->returnType = $entityNamespace . $this->getEntityName(); 666 | if (class_exists($this->returnType)) { 667 | break; 668 | } 669 | } 670 | } 671 | 672 | if (!isset($this->allowedFields) || count($this->allowedFields) == 0) { 673 | $this->allowedFields = ModelDefinitionCache::getFields($this->getEntityName(), $this->table); 674 | } 675 | 676 | $this->afterFind[] = 'handleResult'; 677 | $this->beforeUpdate[] = 'modifyUpdateFields'; 678 | $this->beforeInsert[] = 'modifyInsertFields'; 679 | if(in_array('deletion_id', $this->allowedFields)) { 680 | $this->useSoftDeletes = true; 681 | $this->deletedField = 'deletion_id'; 682 | } 683 | } 684 | 685 | public function getTableName() { 686 | helper('inflector'); 687 | return strtolower(preg_replace('/(?getEntityName()))); 688 | } 689 | 690 | public function getEntityName() { 691 | $namespace = explode('\\', get_class($this)); 692 | return substr(end($namespace), 0, -5); 693 | } 694 | 695 | public function getPrimaryKey() { 696 | return $this->primaryKey; 697 | } 698 | 699 | // 700 | 701 | } 702 | -------------------------------------------------------------------------------- /Hooks/PreController.php: -------------------------------------------------------------------------------- 1 | defaultGroup; 26 | } 27 | 28 | $table = new Table(); 29 | $table->dbGroup = $config->{$group}; 30 | $table->name = $name; 31 | $table->db = Database::connect($group); 32 | $table->forge = Database::forge($group); 33 | return $table; 34 | } 35 | 36 | public function create($primaryKeyName = 'id', $primaryKeyType = ColumnTypes::INT, $autoIncrement = true) { 37 | $sql = "CREATE TABLE IF NOT EXISTS `$this->name` ( 38 | `{$primaryKeyName}` {$primaryKeyType} ".($autoIncrement ? 'AUTO_INCREMENT' : '').", 39 | PRIMARY KEY (`{$primaryKeyName}`) 40 | ) ENGINE=InnoDB ".($autoIncrement ? 'AUTO_INCREMENT=1' : '') 41 | ." CHARACTER SET {$this->dbGroup['charset']} COLLATE {$this->dbGroup['DBCollat']};"; 42 | $this->db->query($sql); 43 | return $this; 44 | } 45 | 46 | public function hasColumn($name) { 47 | $db = $this->db->getDatabase(); 48 | $sql = "SELECT count(*) as count FROM information_schema.columns 49 | WHERE `table_schema` = '{$db}' 50 | AND `table_name` = '{$this->name}' 51 | AND `column_name` = '{$name}';"; 52 | $result = $this->db->query($sql); 53 | return $result->getResultArray()[0]['count']; 54 | } 55 | 56 | public function column($name, $type, $default = null) { 57 | if(!$this->hasColumn($name)) { 58 | $sql = "ALTER TABLE `{$this->name}` ADD `{$name}` {$type}"; 59 | if($default) { 60 | if(is_string($default)) $default = "\"$default\""; 61 | $sql .= " DEFAULT {$default}"; 62 | } 63 | $sql .= ";"; 64 | $this->db->query($sql); 65 | 66 | ModelDefinitionCache::getInstance()->clearCache(); 67 | } 68 | return $this; 69 | } 70 | 71 | public function dropTable() { 72 | $sql = "DROP TABLE IF EXISTS `{$this->name}`"; 73 | $this->db->query($sql); 74 | return $this; 75 | } 76 | 77 | public function truncate() { 78 | if($this->db->resetDataCache()->tableExists($this->name)) { 79 | $this->db->query("TRUNCATE {$this->name}"); 80 | } 81 | } 82 | 83 | public function dropColumn($name) { 84 | if($this->hasColumn($name)) { 85 | $sql = "ALTER TABLE {$this->name} DROP COLUMN {$name};"; 86 | $this->db->query($sql); 87 | } 88 | return $this; 89 | } 90 | 91 | public function timestamps() { 92 | return $this 93 | ->column('created', ColumnTypes::DATETIME) 94 | ->column('updated', ColumnTypes::DATETIME); 95 | } 96 | 97 | public function softDelete() { 98 | return $this 99 | ->column('deletion_id', ColumnTypes::INT) 100 | ->addIndex('deletion_id'); 101 | } 102 | 103 | public function createdUpdatedBy() { 104 | return $this 105 | ->column('created_by_id', ColumnTypes::INT) 106 | ->column('updated_by_id', ColumnTypes::INT); 107 | } 108 | 109 | public function addIndex($names) { 110 | $args = func_get_args(); 111 | $name = $args[0]; 112 | 113 | $indexes = []; 114 | if(count($args) > 1) 115 | $indexes = array_slice($args, 1); 116 | 117 | if(count($indexes) > 0) 118 | $indexes = implode(', ', $indexes); 119 | else 120 | $indexes = "`$name`"; 121 | 122 | if($this->hasIndex($name)) { 123 | //Data::debug("Index $name already exists in {$this->name}"); 124 | } else { 125 | $this->db->query("ALTER TABLE `{$this->name}` ADD INDEX `$name` ($indexes)"); 126 | //Data::debug("Index $name added to {$this->name}"); 127 | } 128 | 129 | return $this; 130 | } 131 | 132 | public function hasIndex($name) { 133 | if(!$this->hasColumn($name)) 134 | Data::debug(get_class($this), "column", $name, 'does not exist on table', $this->name); 135 | $sql = "SELECT TABLE_SCHEMA, COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS 136 | WHERE table_schema=DATABASE() AND table_name='{$this->name}' AND index_name='$name';"; 137 | $result = $this->db->query($sql); 138 | $data = null; 139 | foreach($result->getResult() as $row) { 140 | if(strcmp($row->TABLE_SCHEMA, $this->db->getDatabase()) === 0) 141 | $data = $row; 142 | } 143 | return isset($data) ? $data->IndexIsThere : false; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /ModelParser/ModelItem.php: -------------------------------------------------------------------------------- 1 | path = $path; 28 | 29 | $isEntity = true; 30 | try { 31 | $rc = new \ReflectionClass("\App\Entities\\{$path}"); 32 | try { 33 | $item->isResource = $rc->implementsInterface('\RestExtension\ResourceEntityInterface'); 34 | } catch(Exception $e) { 35 | 36 | } 37 | } catch(\Exception $e) { 38 | try { 39 | $rc = new \ReflectionClass("\App\Interfaces\\{$path}"); 40 | } catch(\Exception $e) { 41 | return false; 42 | } 43 | $isEntity = false; 44 | } 45 | $item->name = substr($rc->getName(), strrpos($rc->getName(), '\\') + 1); 46 | 47 | $comments = $rc->getDocComment(); 48 | $lines = explode("\n", $comments); 49 | $isMany = false; 50 | foreach($lines as $line) { 51 | if(strpos($line, 'Many') !== false) $isMany = true; 52 | if(strpos($line, 'OTF') !== false) $isMany = false; 53 | $property = PropertyItem::parse($line, $isMany); 54 | if($property) 55 | $item->properties[] = $property; 56 | } 57 | 58 | // Append static properties 59 | if($isEntity) { 60 | $item->properties[] = new PropertyItem('id', 'int', true, false); 61 | $item->properties[] = new PropertyItem('created', 'string', true, false); 62 | $item->properties[] = new PropertyItem('updated', 'string', true, false); 63 | $item->properties[] = new PropertyItem('created_by_id', 'int', true, false); 64 | $item->properties[] = new PropertyItem('created_by', 'User', false, false); 65 | $item->properties[] = new PropertyItem('updated_by_id', 'int', true, false); 66 | $item->properties[] = new PropertyItem('updated_by', 'User', false, false); 67 | $item->properties[] = new PropertyItem('deletion_id', 'int', true, false); 68 | $item->properties[] = new PropertyItem('deletion', 'Deletion', false, false); 69 | } 70 | 71 | return $item; 72 | } 73 | 74 | /** 75 | * @return bool|ApiItem 76 | */ 77 | public function getApiItem() { 78 | $entityName = "\App\Entities\\{$this->path}"; 79 | /** @var Entity $entity */ 80 | $entity = new $entityName(); 81 | try { 82 | return ApiItem::parse($entity->getResourcePath()); 83 | } catch(\ReflectionException $e) { 84 | } 85 | return false; 86 | } 87 | 88 | public function toSwagger() { 89 | $item = [ 90 | 'title' => $this->name, 91 | 'type' => 'object', 92 | 'properties' => [] 93 | ]; 94 | foreach($this->properties as $property) { 95 | $item['properties'][$property->name] = $property->toSwagger(); 96 | } 97 | return $item; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /ModelParser/ModelParser.php: -------------------------------------------------------------------------------- 1 | models = $models; 33 | return $parser; 34 | } 35 | 36 | public function generateSwagger($overrideWithStatic = false, $schemaReferences = null, $scope = null) { 37 | $json = []; 38 | 39 | // Append Static Schemas (From Schemas folder) 40 | $schemas = self::loadStatics(); 41 | foreach($schemas as $schema) { 42 | $schemaName = substr($schema, 0, -5); 43 | $json[$schemaName] = json_decode(file_get_contents(self::$staticPath . '/' . $schema)); 44 | } 45 | 46 | // Append schemaReferences recursively 47 | // $list = []; 48 | // foreach($schemaReferences as $schemaReference) { 49 | // $this->appendSchemaReferencesRecursively($schemaReference, $list); 50 | // } 51 | // $schemaReferences = $list; 52 | 53 | if($scope) { 54 | self::$staticPath = self::$staticPath.'/'.$scope; 55 | $schemas = self::loadStatics(); 56 | foreach($schemas as $schema) { 57 | $schemaName = substr($schema, 0, -5); 58 | $json[$schemaName] = json_decode(file_get_contents(self::$staticPath . '/' . $schema)); 59 | } 60 | } 61 | 62 | foreach($this->models as $model) { 63 | if($schemaReferences && !in_array($model->name, $schemaReferences)) continue; 64 | 65 | $staticName = $model->name.'.json'; 66 | if($overrideWithStatic && in_array($staticName, $schemas)) { 67 | $schema = str_replace("\n", '', file_get_contents(self::$staticPath . '/' . $staticName)); 68 | $jsonSchema = json_decode($schema); 69 | $json[$model->name] = $jsonSchema ? $jsonSchema : $schema; 70 | } else 71 | $json[$model->name] = $model->toSwagger(); 72 | } 73 | return $json; 74 | } 75 | 76 | private function appendSchemaReferencesRecursively($name, &$list) { 77 | if(in_array($name, $list)) return; 78 | $list[] = $name; 79 | 80 | $modelItem = ModelItem::parse($name); 81 | foreach($modelItem->properties as $property) { 82 | if(!$property->isSimpleType) 83 | $this->appendSchemaReferencesRecursively($property->typeScriptType, $list); 84 | } 85 | } 86 | 87 | public function generateTypeScript($debug = false) { 88 | if(!file_exists(WRITEPATH.'tmp/models/definitions/')) mkdir(WRITEPATH.'tmp/models/definitions/', 0777, true); 89 | 90 | $renderer = Services::renderer(__DIR__.'/TypeScript', null, false); 91 | 92 | foreach($this->models as $model) { 93 | Data::debug($model->name.' with '.count($model->properties).' properties'); 94 | 95 | // Definition 96 | $content = $renderer->setData(['model' => $model], 'raw')->render('ModelDefinition', ['debug' => false], null); 97 | if($debug) echo $content; 98 | else 99 | file_put_contents(WRITEPATH.'tmp/models/definitions/'.$model->name.'Definition.ts', $content); 100 | 101 | // Model 102 | $content = $renderer->setData(['model' => $model], 'raw')->render('Model', ['debug' => false], null); 103 | if($debug) echo $content; 104 | else 105 | file_put_contents(WRITEPATH.'tmp/models/'.$model->name.'.ts', $content); 106 | 107 | } 108 | 109 | $content = $renderer->setData(['models' => $this->models], 'raw')->render('Index', ['debug' => false], null); 110 | file_put_contents(WRITEPATH.'tmp/models/index.ts', $content); 111 | } 112 | 113 | public function generateXamarin($debug = false) { 114 | if(!file_exists(WRITEPATH.'tmp/xamarin/models/Definitions/')) mkdir(WRITEPATH.'tmp/xamarin/models/Definitions/', 0777, true); 115 | 116 | $renderer = Services::renderer(__DIR__.'/Xamarin', null, false); 117 | 118 | foreach($this->models as $model) { 119 | Data::debug($model->name.' with '.count($model->properties).' properties'); 120 | 121 | // Definition 122 | $content = $renderer->setData(['model' => $model], 'raw')->render('ModelDefinition', ['debug' => false], null); 123 | if($debug) echo $content; 124 | else 125 | file_put_contents(WRITEPATH.'tmp/xamarin/models/Definitions/'.$model->name.'Definition.cs', $content); 126 | 127 | // Model 128 | $content = $renderer->setData(['model' => $model], 'raw')->render('Model', ['debug' => false], null); 129 | if($debug) echo $content; 130 | else 131 | file_put_contents(WRITEPATH.'tmp/xamarin/models/'.$model->name.'.cs', $content); 132 | 133 | } 134 | } 135 | 136 | 137 | /** 138 | * @param $model 139 | * @return ModelItem 140 | */ 141 | private static function parseModels($model) { 142 | return ModelItem::parse(substr($model, 0, -4)); 143 | } 144 | 145 | private static function loadModels($includeInterfaces = false) { 146 | $files = scandir(APPPATH. 'Entities'); 147 | if($includeInterfaces && is_dir(APPPATH. 'Interfaces')) { 148 | $files = array_merge($files, scandir(APPPATH . 'Interfaces')); 149 | } 150 | 151 | $models = []; 152 | foreach($files as $file) { 153 | if($file[0] != '_' && substr($file, -3) == 'php') { 154 | $models[] = $file; 155 | } 156 | } 157 | return $models; 158 | } 159 | 160 | private static function loadStatics() { 161 | $schemas = []; 162 | if(is_dir(self::$staticPath)) { 163 | $files = scandir(self::$staticPath); 164 | foreach($files as $file) { 165 | if($file[0] != '_' && substr($file, -4) == 'json') { 166 | $schemas[] = $file; 167 | } 168 | } 169 | } 170 | return $schemas; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /ModelParser/PropertyItem.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->typeScriptType = $type; 24 | $this->xamarinType = $type; 25 | $this->isSimpleType = $isSimpleType; 26 | $this->isMany = $isMany; 27 | $this->setType($type); 28 | } 29 | 30 | public static function validate($line) { 31 | if(strpos($line, '@property') === false) return false; 32 | $line = substr($line, strlen(' * @property ')); 33 | $parts = explode(' ', $line); 34 | if(count($parts) < 2) return false; 35 | return $parts; 36 | } 37 | 38 | public static function parse($line, $isMany = false) { 39 | $parts = PropertyItem::validate($line); 40 | if(!$parts) return false; 41 | 42 | $type = array_shift($parts); 43 | $item = new PropertyItem(); 44 | $item->name = substr(array_shift($parts), 1); 45 | $item->isMany = $isMany; 46 | 47 | if(count($parts)) 48 | $item->comment = implode(' ', $parts); 49 | 50 | $item->setType($type); 51 | 52 | return $item; 53 | } 54 | 55 | public function setType($type) { 56 | $this->isSimpleType = true; 57 | 58 | $this->rawType = $type; 59 | 60 | switch($type) { 61 | case 'int': 62 | case 'double': 63 | $this->typeScriptType = 'number'; 64 | break; 65 | case 'string|double': 66 | $this->typeScriptType = 'string'; 67 | break; 68 | case 'boolean': 69 | case 'bool': 70 | $this->typeScriptType = 'boolean'; 71 | break; 72 | case 'string': 73 | $this->typeScriptType = 'string'; 74 | break; 75 | case 'int[]': 76 | $this->typeScriptType = 'number[]'; 77 | break; 78 | default: 79 | $this->typeScriptType = $type; 80 | $this->isSimpleType = false; 81 | break; 82 | } 83 | // Xamarin 84 | switch($type) { 85 | case 'string|double': 86 | $this->xamarinType = 'string'; 87 | break; 88 | case 'boolean': 89 | case 'bool': 90 | $this->xamarinType = 'bool'; 91 | break; 92 | default: 93 | $this->xamarinType = $type; 94 | break; 95 | } 96 | } 97 | 98 | public function toSwagger() { 99 | $item = []; 100 | 101 | $type = $this->rawType; 102 | switch($this->rawType) { 103 | case 'int[]': 104 | $type = 'integer[]'; 105 | break; 106 | case 'int': 107 | $type = 'integer'; 108 | break; 109 | case 'bool': 110 | $type = 'boolean'; 111 | break; 112 | } 113 | 114 | if($this->isSimpleType) 115 | $item['type'] = $type; 116 | else 117 | $item['type'] = "{$type}"; //"#/components/schemas/{$this->type}"; 118 | 119 | if(strpos($type, '[]') !== false) { 120 | $item['items'] = ['type' => substr($item['type'], 0, -2)]; 121 | $item['type'] = 'array'; 122 | } 123 | 124 | return $item; 125 | } 126 | 127 | public function getCamelName() { 128 | return camelize($this->name); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /ModelParser/TypeScript/Index.php: -------------------------------------------------------------------------------- 1 | 5 | export {name?> as name?>} from "./name?>"; 6 | 7 | -------------------------------------------------------------------------------- /ModelParser/TypeScript/Model.php: -------------------------------------------------------------------------------- 1 | 5 | /** 6 | * Created by ModelParser 7 | * Date: . 8 | * Time: . 9 | */ 10 | import {name?>Definition} from './definitions/name?>Definition'; 11 | 12 | export class name?> extends name?>Definition { 13 | 14 | constructor(json?: any) { 15 | super(json); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ModelParser/TypeScript/ModelDefinition.php: -------------------------------------------------------------------------------- 1 | 4 | /** 5 | * Created by ModelParser 6 | */ 7 | properties as $property) : ?> 11 | isSimpleType && !in_array($property->typeScriptType, $imported)) : 12 | $imported[] = $property->typeScriptType; ?> 13 | import {typeScriptType?>} from '../typeScriptType?>'; 14 | 15 | 17 | import {BaseModel} from '../BaseModel'; 18 | isResource && $model->getApiItem()) { ?> 19 | import {Api} from '../../http/Api/Api'; 20 | 21 | 22 | export class name?>Definition extends BaseModel { 23 | properties as $property) : ?> 24 | name?>?: typeScriptType?>isSimpleType?'':''?>isMany?"[]":""?>; 25 | 26 | 27 | constructor(data?: any) { 28 | super(); 29 | this.populate(data); 30 | } 31 | 32 | public populate(data?: any, patch = false) { 33 | if (!patch) { 34 | properties as $property) : ?> 35 | delete this.name?>; 36 | 37 | } 38 | 39 | if (!data) return; 40 | properties as $property) : ?> 41 | if (data.name?> != null) { 42 | isMany): ?> 43 | this.name?> = data.name?>.map((i: any) => new typeScriptType?>(i)); 44 | 45 | isSimpleType): ?> 46 | this.name?> = data.name?>; 47 | 48 | this.name?> = new typeScriptType?>(data.name?>); 49 | 50 | 51 | } 52 | 53 | } 54 | isResource && $model->getApiItem()) { ?> 55 | getApiItem()->generateTypeScriptModelFunctions()?> 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ModelParser/TypeScript/ModelInterface.php: -------------------------------------------------------------------------------- 1 | 5 | /** 6 | * Created by ModelParser 7 | * Date: . 8 | * Time: . 9 | */ 10 | properties as $property) : ?> 14 | isSimpleType && !in_array($property->typeScriptType, $imported) && $property->typeScriptType != $model->name) : 15 | $imported[] = $property->typeScriptType; ?> 16 | import {typeScriptType?>Interface} from "./typeScriptType?>Interface"; 17 | 18 | 20 | 21 | export interface name?>Interface { 22 | properties as $property) : ?> 23 | name?>?: typeScriptType?>isSimpleType?'':'Interface'?>isMany?"[]":""?>; 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /ModelParser/Xamarin/Model.php: -------------------------------------------------------------------------------- 1 | 5 | /** 6 | * Created by ModelParser 7 | * Date: . 8 | * Time: . 9 | */ 10 | using System; 11 | using xamarinModelsNamespace?>.Definitions; 12 | 13 | namespace xamarinModelsNamespace?> 14 | { 15 | public class name?> : name?>Definition 16 | { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ModelParser/Xamarin/ModelDefinition.php: -------------------------------------------------------------------------------- 1 | 4 | /** 5 | * Created by ModelParser 6 | */ 7 | using System.Collections.Generic; 8 | using Newtonsoft.Json; 9 | using xamarinBaseModelNamespace?>; 10 | 11 | namespace xamarinModelsNamespace?>.Definitions 12 | { 13 | 14 | [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] 15 | public class name?>Definition : BaseModel 16 | { 17 | properties as $property) : ?> 18 | 19 | [JsonProperty("name?>")] 20 | isMany) { ?> 21 | public List<xamarinType?>> getCamelName())?> { get; set; } 22 | 23 | public xamarinType?> getCamelName())?> { get; set; } 24 | 25 | 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodeIgniter 4 OrmExtension 2 | 3 | [![Latest Stable Version](http://poser.pugx.org/4spacesdk/ci4ormextension/v)](https://packagist.org/packages/4spacesdk/ci4ormextension) [![Total Downloads](http://poser.pugx.org/4spacesdk/ci4ormextension/downloads)](https://packagist.org/packages/4spacesdk/ci4ormextension) [![Latest Unstable Version](http://poser.pugx.org/4spacesdk/ci4ormextension/v/unstable)](https://packagist.org/packages/4spacesdk/ci4ormextension) [![License](http://poser.pugx.org/4spacesdk/ci4ormextension/license)](https://packagist.org/packages/4spacesdk/ci4ormextension) [![PHP Version Require](http://poser.pugx.org/4spacesdk/ci4ormextension/require/php)](https://packagist.org/packages/4spacesdk/ci4ormextension) 4 | 5 | ## What is CodeIgniter 4 OrmExtension? 6 | OrmExtension is an Object Relational Mapper written in PHP for CodeIgniter 4. 7 | It is designed to map your Database tables into easy to work with objects, fully aware of the relationships between each other. 8 | OrmExtension is based on the same idea as the original [WanWizard DataMapper](https://datamapper.wanwizard.eu/) for CodeIgniter 2. But totally rewritten to fit CodeIgniter 4. 9 | 10 | 11 | ## Installation 12 | Step 1) 13 | 14 | `composer require 4spacesdk/ci4ormextension` 15 | 16 | Step 2) 17 | 18 | Create new file `app/Config/OrmExtension.php` and add this content 19 | ```php 20 | whereRelated(ColorModel::class, 'name', 'green') 207 | ->find(); 208 | ``` 209 | Select users with the role admin and color blue: 210 | ```php 211 | $userModel = new UserModel(); 212 | $users = $userModel 213 | ->whereRelated(RoleModel::class, 'name', 'admin') 214 | ->whereRelated(ColorModel::class, 'name', 'blue') 215 | ->find(); 216 | ``` 217 | Select all users and include their color: 218 | ```php 219 | $userModel = new UserModel(); 220 | $users = $userModel 221 | ->includeRelated(ColorModel::class) 222 | ->find(); 223 | ``` 224 | Select users with role admin and include their color: 225 | ```php 226 | $userModel = new UserModel(); 227 | $users = $userModel 228 | ->includeRelated(ColorModel::class) 229 | ->whereRelated(RoleModel::class, 'name', 'admin') 230 | ->find(); 231 | ``` 232 | 233 | #### Result 234 | The return from `find()` has been changed. `Find` will always return a entity class related to the calling model. It will never be null or an array of entities. This is a good think - because now we have some consistent to work with. The entity is traversable, so we can use it in a loop! 235 | Check these examples: 236 | ```php 237 | $userModel = new UserModel(); 238 | $user = $userModel 239 | ->where('id', 1) 240 | ->find(); 241 | echo json_encode($user->toArray()); 242 | ``` 243 | ```json 244 | { 245 | "id": 1, 246 | "name": "Martin" 247 | } 248 | ``` 249 | 250 | ```php 251 | $userModel = new UserModel(); 252 | $users = $userModel->find(); 253 | echo json_encode($users->allToArray()); 254 | ``` 255 | ```json 256 | [ 257 | { 258 | "id": 1, 259 | "name": "Martin" 260 | }, 261 | { 262 | "id": 2, 263 | "name": "Kevin" 264 | } 265 | ] 266 | ``` 267 | 268 | `toArray()` returns an array with one user's properties. `allToArray()` returns an array of multiple user's properties. These methods are great for json encoding. 269 | 270 | 271 | ### Working with Entities 272 | Relations can be accessed as magic properties. 273 | This will echo null, because the color is an empty entity. It has not yet been retrieved from the database. 274 | ```php 275 | $userModel = new UserModel(); 276 | $users = $userModel->find(); 277 | foreach($users as $user) { 278 | echo $user->color->name; 279 | } 280 | ``` 281 | 282 | We can retrieve the color with an include: 283 | ```php 284 | $userModel = new UserModel(); 285 | $users = $userModel 286 | ->includeRelated(ColorModel::class) 287 | ->find(); 288 | foreach($users as $user) { 289 | echo $user->color->name; 290 | } 291 | ``` 292 | This will echo the actual color name, because OrmExtension has prefetched the color from the `find`. 293 | 294 | We can also retrieve the color afterwards: 295 | ```php 296 | $userModel = new UserModel(); 297 | $users = $userModel->find(); 298 | foreach($users as $user) { 299 | $user->color->find(); 300 | echo $user->color->name; 301 | } 302 | ``` 303 | 304 | A user can have multiple roles and we want to access only the role named admin. For this we have to access the model from the entity to do a `where`. 305 | ```php 306 | $userModel = new UserModel(); 307 | $user = $userModel->find(1); 308 | $role = $user->roles->_getModel() 309 | ->where('name', 'admin') 310 | ->find(); 311 | echo $role->name; // "admin" 312 | ``` 313 | 314 | 315 | ### Deep relations 316 | For this purpose we will look at another example. Let's say an `user` has `books` and `books` has `color`. A `book` is shared between many users but can only have one color. A color can be shared between many books. 317 | ```php 318 | class UserModel { 319 | public $hasMany = [ 320 | BookModel::class 321 | ]; 322 | } 323 | class BookModel { 324 | public $hasOne = [ 325 | ColorModel::class 326 | ], 327 | public $hasMany = [ 328 | UserModel::class 329 | ]; 330 | } 331 | class ColorModel { 332 | public $hasMany = [ 333 | BookModel::class 334 | ]; 335 | } 336 | ``` 337 | 338 | We want to select all users with green books. 339 | ```php 340 | $userModel = new UserModel(); 341 | $users = $userModel 342 | ->whereRelated([BookModel::class, ColorModel::class], 'name', 'green') 343 | ->find(); 344 | ``` 345 | To access deep relations, simply put them in an array. 346 | 347 | 348 | ### Soft deletion 349 | OrmExtension provides an extended soft deletion. Create a model and entity for `Deletion`. 350 | **Entity** 351 | ```php 352 | generateSwagger(); 397 | ``` 398 | Attach `$schemes` to swagger components and you have all your models documented. 399 | ```php 400 | $parser = ModelParser::run(); 401 | $parser->generateTypeScript(); 402 | ``` 403 | This will generate typescript models as classes and interfaces. Find the files under `writeable/tmp`. 404 | 405 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "4spacesdk/ci4ormextension", 3 | "description": "ORM Extension for CodeIgniter 4", 4 | "authors": [ 5 | { 6 | "name": "Martin Hansen", 7 | "email": "martin@4spaces.dk" 8 | } 9 | ], 10 | "license": "MIT", 11 | "require": { 12 | "php": ">=7.1", 13 | "4spacesdk/ci4debugtool": ">=1.0.0" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "OrmExtension\\": "" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /debug_helper.php: -------------------------------------------------------------------------------- 1 | log('notice', $msg); 9 | } 10 | 11 | }; 12 | --------------------------------------------------------------------------------