public function test_assemble() { $path = '[element]->property'; $this->assertSame(PropertyPath::assemble(PropertyPath::parse($path)), $path); $path = 'any[element]->property'; $this->assertSame(PropertyPath::assemble(PropertyPath::parse($path)), $path); $path = '->property[element].any'; $this->assertSame(PropertyPath::assemble(PropertyPath::parse($path)), $path); }
/** * Register all models from the backend. * Aslo validates and corrects the model configurations. * * @param RepositoryBackend $backend */ public function registerBackend($backend) { if ($backend->identifier === null) { throw new Exception('RepositoryBackend->identifier is required'); } if (count($backend->configs) === 0) { notice(get_class($backend) . ': "' . $backend->identifier . '" doesn\'t have any ModelConfigs'); return; } if (isset($this->backends[$backend->identifier])) { throw new Exception('RepositoryBackend "' . $backend->identifier . '" already registered'); } $this->backends[$backend->identifier] = $backend; // Pass 1: Register configs foreach ($backend->configs as $config) { if ($config->backend === null) { $config->backend = $backend->identifier; } $this->registerModel($config); } // Pass 2: Register junctions foreach ($backend->junctions as $junction) { if ($junction->backend === null) { $junction->backend = $backend->identifier; } if (isset($this->junctions[$junction->name])) { notice('overwriting junction ' . $junction->name); } $this->junctions[$junction->name] = $junction; } // Pass 3: Auto detect id's foreach ($backend->configs as $backendConfig) { $config = $this->configs[$backendConfig->name]; if (count($config->id) === 0) { if (isset($config->properties['id'])) { // No id set, but the column 'id' exists? $config->id = array('id'); } else { warning('Invalid config: ' . $config->name . '->id is not configured and could not detect an "id" element'); } } } // Pass 4: Validate and correct configs foreach ($backend->configs as $backendConfig) { $config = $this->configs[$backendConfig->name]; if (count($config->properties) === 0) { warning('Invalid config: ' . $config->name . '->properties array is not configured'); } foreach ($config->id as $idIndex => $idColumn) { if (isset($config->properties[$idColumn])) { $idProperty = $config->properties[$idColumn]; } else { warning('Invalid config: ' . $config->name . '->id[' . $idIndex . ']: "' . $idColumn . '" isn\'t mapped as a property'); } } foreach ($config->belongsTo as $property => $belongsTo) { $validationError = false; if (is_array($belongsTo) === false) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '] should be an array'; } if (empty($belongsTo['model'])) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '][model] not set'; } elseif (empty($belongsTo['reference']) && empty($belongsTo['convert'])) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '] is missing a [reference] or [convert] element'; } elseif (isset($belongsTo['convert']) && isset($belongsTo['reference'])) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '] can\'t contain both a [reference] and a [convert] element'; } if (isset($belongsTo['reference'])) { if (empty($belongsTo['id'])) { // id not set, but (target)model is configured? if (empty($this->configs[$belongsTo['model']])) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '][id] couldn\'t be inferred, because model "' . $belongsTo['model'] . '" isn\'t registered'; } else { $belongsToConfig = $this->_getConfig($belongsTo['model']); // Infer/Assume that the id is the ID from the model if (count($belongsToConfig->id) == 1) { $belongsTo['id'] = current($belongsToConfig->id); $config->belongsTo[$property]['id'] = $belongsTo['id']; // Update config } else { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '][id] not set and can\'t be inferred (for a complex key)'; } } } if (isset($belongsTo['reference']) && isset($belongsTo['useIndex']) == false) { if (empty($this->configs[$belongsTo['model']])) { $validationError = 'Invalid config: ' . $config->name . '->belongsTo[' . $property . '][useIndex] couldn\'t be inferred, because model "' . $belongsTo['model'] . '" isn\'t registered'; } else { $belongsToConfig = $this->_getConfig($belongsTo['model']); // Is the foreign key is linked to the model id $belongsTo['useIndex'] = count($belongsToConfig->id) == 1 && $belongsTo['id'] == current($belongsToConfig->id); $config->belongsTo[$property]['useIndex'] = $belongsTo['useIndex']; // Update config } } if (isset($belongsTo['id'])) { // Add foreign key to the collection mapping $this->collectionMappings[$config->name][$property . '->' . $belongsToConfig->properties[$belongsTo['id']]] = $belongsTo['reference']; $this->collectionMappings[$config->name][$property . '.' . $belongsToConfig->properties[$belongsTo['id']]] = $belongsTo['reference']; } } // @todo Add collectionMapping for "convert" relations? if (empty($this->configs[$belongsTo['model']])) { // $validationError = 'Invalid config: '.$config->name.'->belongsTo['.$property.'][model] "'.$belongsTo['model'].'" isn\'t registered'; } // Remove invalid relations if ($validationError) { warning($validationError); unset($config->belongsTo[$property]); } } foreach ($config->hasMany as $property => $hasMany) { $validationError = false; if (empty($hasMany['model'])) { $validationError = 'Invalid config: ' . $config->name . '->hasMany[' . $property . '][model] not set'; } elseif (isset($hasMany['convert'])) { // no additional fields are needed. } elseif (empty($hasMany['reference'])) { // @todo Infer property (lookup belongsTo) $validationError = 'Invalid hasMany: ' . $config->name . '->hasMany[' . $property . '][reference] not set'; } elseif (isset($hasMany['reference']) && empty($hasMany['belongsTo'])) { $referencePath = PropertyPath::parse($hasMany['reference']); if (count($referencePath) == 1) { // The foreign key is linked directly } elseif (empty($this->configs[$hasMany['model']])) { $validationError = 'Invalid config: ' . $config->name . '->hasMany[' . $property . '][belongsTo] couldn\'t be inferred, because model "' . $hasMany['model'] . '" isn\'t registered'; } else { // Infer the belongsTo path based on the model and reference path. $hasManyConfig = $this->configs[$hasMany['model']]; $idProperty = array(array_value(array_pop($referencePath), 1)); if ($idProperty == $hasManyConfig->id) { $hasMany['belongsTo'] = PropertyPath::assemble($referencePath); $config->hasMany[$property]['belongsTo'] = $hasMany['belongsTo']; // update config } } } if (isset($hasMany['through'])) { if (array_key_exists('fields', $hasMany) === false) { $config->hasMany[$property]['fields'] = []; } elseif (array_key_exists('junctionClass', $hasMany) === false) { $config->hasMany[$property]['junctionClass'] = Junction::class; } $junctionConfig = @$this->junctions[$hasMany['through']]; if (array_key_exists('idPath', $hasMany) === false && $junctionConfig) { // dump($junctionConfig); } } // Remove invalid relations if ($validationError) { warning($validationError); unset($config->hasMany[$property]); } } // Validate read & write filters foreach ($config->readFilters as $column => $filter) { if (empty($config->properties[$column])) { notice('Invalid config: ' . $config->name . '->readFilters[' . $column . '] isn\'t mapped as property', $filter); } } foreach ($config->writeFilters as $column => $filter) { if (empty($config->properties[$column])) { notice('Invalid config: ' . $config->name . '->writeFilters[' . $column . '] isn\'t mapped as property', $filter); } } } // Pass 5: Generate classes based on properties when no class is detected/found. foreach ($backend->configs as $config) { if (substr($config->class, 0, 11) !== '\\Generated\\') { $this->validated[$config->name] = false; } else { $this->validated[$config->name] = true; // Generated classes are valid by design. if (class_exists($config->class, false)) { notice('Skipped generating class: "' . $config->class . '", a class with the same name exists'); continue; } $parts = explode('\\', $config->class); array_pop($parts); // remove class part. $namespace = implode('\\', array_slice($parts, 1)); // Generate class $use = ''; $aliases = []; $alias = $this->buildAlias(Object::class, $aliases, $use); $php = "\nclass " . $config->name . " extends " . $alias . "\n{\n"; $properties = []; foreach ($config->properties as $path) { $parsedPath = PropertyPath::parse($path); $property = $parsedPath[0][1]; if (!in_array($property, $properties)) { $php .= " public \$" . $property . ";\n"; $properties[] = $property; } } foreach ($config->belongsTo as $path => $belongsTo) { $parsedPath = PropertyPath::parse($path); $belongsToConfig = $this->_getConfig($belongsTo['model']); $alias = $this->buildAlias($belongsToConfig->class, $aliases, $use); $property = $parsedPath[0][1]; $php .= "\n"; $php .= " /**\n"; $php .= " * @var " . $alias . ' The associated ' . $belongsToConfig->name . "\n"; $php .= " */\n"; $php .= " public \$" . $property . ";\n"; } foreach ($config->hasMany as $path => $hasMany) { $parsedPath = PropertyPath::parse($path); $hasManyConfig = $this->_getConfig($hasMany['model']); $property = $parsedPath[0][1]; $aliasC = $this->buildAlias(Collection::class, $aliases, $use); $alias = $this->buildAlias($hasManyConfig->class, $aliases, $use); $php .= "\n"; $php .= " /**\n"; $php .= " * @var " . $aliasC . "|" . $alias . "[] A collection with the associated " . $hasManyConfig->plural . "\n"; $php .= " */\n"; $php .= " public \$" . $property . ";\n"; } $php .= '}'; $php = "namespace " . $namespace . ";\n\n" . $use . $php; if (\Sledgehammer\ENVIRONMENT === 'development' && $namespace === 'Generated') { // Write autoComplete helper // @todo Only write file when needed, aka validate $this->autoComplete \Sledgehammer\mkdirs(\Sledgehammer\TMP_DIR . 'AutoComplete'); file_put_contents(\Sledgehammer\TMP_DIR . 'AutoComplete/' . $config->name . '.php', "<?php\n\n" . $php . "\n"); } eval($php); } } // Pass 6: Generate or update the AutoComplete Helper for the default repository? if (\Sledgehammer\ENVIRONMENT == 'development' && isset(self::$instances['default']) && self::$instances['default'] === $this) { $autoCompleteFile = \Sledgehammer\TMP_DIR . 'AutoComplete/repository.ini'; if ($this->autoComplete === null) { if (file_exists($autoCompleteFile)) { $this->autoComplete = parse_ini_file($autoCompleteFile, true); } else { $this->autoComplete = []; } } // Validate AutoCompleteHelper foreach ($backend->configs as $config) { $autoComplete = array('class' => $config->class, 'properties' => implode(', ', $config->properties)); if (empty($this->autoComplete[$config->name]) || $this->autoComplete[$config->name] != $autoComplete) { $this->autoComplete[$config->name] = $autoComplete; \Sledgehammer\mkdirs(\Sledgehammer\TMP_DIR . 'AutoComplete'); \Sledgehammer\write_ini_file($autoCompleteFile, $this->autoComplete, 'Repository AutoComplete config'); $this->writeAutoCompleteHelper(\Sledgehammer\TMP_DIR . 'AutoComplete/DefaultRepository.php', 'DefaultRepository', 'Generated'); } } } }