* @version $Revision: 1716 $ * @package propel.generator.task */ class PropelSchemaReverseTask extends PDOTask { /** * Zero bit for no validators */ const VALIDATORS_NONE = 0; /** * Bit for maxLength validator */ const VALIDATORS_MAXLENGTH = 1; /** * Bit for maxValue validator */ const VALIDATORS_MAXVALUE = 2; /** * Bit for type validator */ const VALIDATORS_TYPE = 4; /** * Bit for required validator */ const VALIDATORS_REQUIRED = 8; /** * Bit for unique validator */ const VALIDATORS_UNIQUE = 16; /** * Bit for all validators */ const VALIDATORS_ALL = 255; /** * File to contain XML database schema. * @var PhingFIle */ protected $xmlSchema; /** * DB encoding to use * @var string */ protected $dbEncoding = 'iso-8859-1'; /** * DB schema to use. * @var string */ protected $dbSchema; /** * The datasource name (used for in schema.xml) * * @var string */ protected $databaseName; /** * DOM document produced. * @var DOMDocument */ protected $doc; /** * The document root element. * @var DOMElement */ protected $databaseNode; /** * Hashtable of columns that have primary keys. * @var array */ protected $primaryKeys; /** * Whether to use same name for phpName or not. * @var boolean */ protected $samePhpName; /** * whether to add vendor info or not * @var boolean */ protected $addVendorInfo; /** * Bitfield to switch on/off which validators will be created. * * @var int */ protected $validatorBits = PropelSchemaReverseTask::VALIDATORS_NONE; /** * Collect validatorInfos to create validators. * * @var int */ protected $validatorInfos; /** * An initialized GeneratorConfig object containing the converted Phing props. * * @var GeneratorConfig */ private $generatorConfig; /** * Maps validator type tokens to bits * * The tokens are used in the propel.addValidators property to define * which validators are to be added * * @var array */ static protected $validatorBitMap = array ( 'none' => PropelSchemaReverseTask::VALIDATORS_NONE, 'maxlength' => PropelSchemaReverseTask::VALIDATORS_MAXLENGTH, 'maxvalue' => PropelSchemaReverseTask::VALIDATORS_MAXVALUE, 'type' => PropelSchemaReverseTask::VALIDATORS_TYPE, 'required' => PropelSchemaReverseTask::VALIDATORS_REQUIRED, 'unique' => PropelSchemaReverseTask::VALIDATORS_UNIQUE, 'all' => PropelSchemaReverseTask::VALIDATORS_ALL, ); /** * Defines messages that are added to validators * * @var array */ static protected $validatorMessages = array ( 'maxlength' => array ( 'msg' => 'The field %s must be not longer than %s characters.', 'var' => array('colName', 'value') ), 'maxvalue' => array ( 'msg' => 'The field %s must be not greater than %s.', 'var' => array('colName', 'value') ), 'type' => array ( 'msg' => 'The column %s must be an %s value.', 'var' => array('colName', 'value') ), 'required' => array ( 'msg' => 'The field %s is required.', 'var' => array('colName') ), 'unique' => array ( 'msg' => 'This %s already exists in table %s.', 'var' => array('colName', 'tableName') ), ); /** * Gets the (optional) schema name to use. * * @return string */ public function getDbSchema() { return $this->dbSchema; } /** * Sets the name of a database schema to use (optional). * * @param string $dbSchema */ public function setDbSchema($dbSchema) { $this->dbSchema = $dbSchema; } /** * Gets the database encoding. * * @return string */ public function getDbEncoding($v) { return $this->dbEncoding; } /** * Sets the database encoding. * * @param string $v */ public function setDbEncoding($v) { $this->dbEncoding = $v; } /** * Gets the datasource name. * * @return string */ public function getDatabaseName() { return $this->databaseName; } /** * Sets the datasource name. * * This will be used as the value in the generated schema.xml * * @param string $v */ public function setDatabaseName($v) { $this->databaseName = $v; } /** * Sets the output name for the XML file. * * @param PhingFile $v */ public function setOutputFile(PhingFile $v) { $this->xmlSchema = $v; } /** * Set whether to use the column name as phpName without any translation. * * @param boolean $v */ public function setSamePhpName($v) { $this->samePhpName = $v; } /** * Set whether to add vendor info to the schema. * * @param boolean $v */ public function setAddVendorInfo($v) { $this->addVendorInfo = (boolean) $v; } /** * Sets set validator bitfield from a comma-separated list of "validator bit" names. * * @param string $v The comma-separated list of which validators to add. * @return void */ public function setAddValidators($v) { $validKeys = array_keys(self::$validatorBitMap); // lowercase input $v = strtolower($v); $bits = self::VALIDATORS_NONE; $exprs = explode(',', $v); foreach ($exprs as $expr) { $expr = trim($expr); if(!empty($expr)) { if (!isset(self::$validatorBitMap[$expr])) { throw new BuildException("Unable to interpret validator in expression ('$v'): " . $expr); } $bits |= self::$validatorBitMap[$expr]; } } $this->validatorBits = $bits; } /** * Checks whether to add validators of specified type or not * * @param int $type The validator type constant. * @return boolean */ protected function isValidatorRequired($type) { return (($this->validatorBits & $type) === $type); } /** * Whether to use the column name as phpName without any translation. * * @return boolean */ public function isSamePhpName() { return $this->samePhpName; } /** * @throws BuildException */ public function main() { if (!$this->getDatabaseName()) { throw new BuildException("databaseName attribute is required for schema reverse engineering", $this->getLocation()); } //(not yet supported) $this->log("schema : " . $this->dbSchema); //DocumentTypeImpl docType = new DocumentTypeImpl(null, "database", null, // "http://jakarta.apache.org/turbine/dtd/database.dtd"); $this->doc = new DOMDocument('1.0', 'utf-8'); $this->doc->formatOutput = true; // pretty printing $this->doc->appendChild($this->doc->createComment("Autogenerated by ".get_class($this)." class.")); try { $database = $this->buildModel(); if ($this->validatorBits !== self::VALIDATORS_NONE) { $this->addValidators($database); } $database->appendXml($this->doc); $this->log("Writing XML to file: " . $this->xmlSchema->getPath()); $out = new FileWriter($this->xmlSchema); $xmlstr = $this->doc->saveXML(); $out->write($xmlstr); $out->close(); } catch (Exception $e) { $this->log("There was an error building XML from metadata: " . $e->getMessage(), Project::MSG_ERR); } $this->log("Schema reverse engineering finished"); } /** * Gets the GeneratorConfig object for this task or creates it on-demand. * @return GeneratorConfig */ protected function getGeneratorConfig() { if ($this->generatorConfig === null) { $this->generatorConfig = new GeneratorConfig(); $this->generatorConfig->setBuildProperties($this->getProject()->getProperties()); } return $this->generatorConfig; } /** * Builds the model classes from the database schema. * @return Database The built-out Database (with all tables, etc.) */ protected function buildModel() { $config = $this->getGeneratorConfig(); $con = $this->getConnection(); $database = new Database($this->getDatabaseName()); $database->setPlatform($config->getConfiguredPlatform($con)); // Some defaults ... $database->setDefaultIdMethod(IDMethod::NATIVE); $parser = $config->getConfiguredSchemaParser($con); $nbTables = $parser->parse($database, $this); $this->log("Successfully Reverse Engineered " . $nbTables . " tables"); return $database; } /** * Adds any requested validators to the data model. * * We will add the following type specific validators: * * for notNull columns: required validator * for unique indexes: unique validator * for varchar types: maxLength validators (CHAR, VARCHAR, LONGVARCHAR) * for numeric types: maxValue validators (BIGINT, SMALLINT, TINYINT, INTEGER, FLOAT, DOUBLE, NUMERIC, DECIMAL, REAL) * for integer and timestamp types: notMatch validator with [^\d]+ (BIGINT, SMALLINT, TINYINT, INTEGER, TIMESTAMP) * for float types: notMatch validator with [^\d\.]+ (FLOAT, DOUBLE, NUMERIC, DECIMAL, REAL) * * @param Database $database The Database model. * @return void * @todo find out how to evaluate the appropriate size and adjust maxValue rule values appropriate * @todo find out if float type column values must always notMatch('[^\d\.]+'), i.e. digits and point for any db vendor, language etc. */ protected function addValidators(Database $database) { $platform = $this->getGeneratorConfig()->getConfiguredPlatform(); foreach ($database->getTables() as $table) { $set = new PropelSchemaReverse_ValidatorSet(); foreach ($table->getColumns() as $col) { if ($col->isNotNull() && $this->isValidatorRequired(self::VALIDATORS_REQUIRED)) { $validator = $set->getValidator($col); $validator->addRule($this->getValidatorRule($col, 'required')); } if (in_array($col->getType(), array(PropelTypes::CHAR, PropelTypes::VARCHAR, PropelTypes::LONGVARCHAR)) && $col->getSize() && $this->isValidatorRequired(self::VALIDATORS_MAXLENGTH)) { $validator = $set->getValidator($col); $validator->addRule($this->getValidatorRule($col, 'maxLength', $col->getSize())); } if ($col->isNumericType() && $this->isValidatorRequired(self::VALIDATORS_MAXVALUE)) { $this->log("WARNING: maxValue validator added for column ".$col->getName().". You will have to adjust the size value manually.", Project::MSG_WARN); $validator = $set->getValidator($col); $validator->addRule($this->getValidatorRule($col, 'maxValue', 'REPLACEME')); } if ($col->isPhpPrimitiveType() && $this->isValidatorRequired(self::VALIDATORS_TYPE)) { $validator = $set->getValidator($col); $validator->addRule($this->getValidatorRule($col, 'type', $col->getPhpType())); } } foreach ($table->getUnices() as $unique) { $colnames = $unique->getColumns(); if (count($colnames) == 1) { // currently 'unique' validator only works w/ single columns. $col = $table->getColumn($colnames[0]); $validator = $set->getValidator($col); $validator->addRule($this->getValidatorRule($col, 'unique')); } } foreach ($set->getValidators() as $validator) { $table->addValidator($validator); } } // foreach table } /** * Gets validator rule for specified type (string). * * @param Column $column The column that is being validated. * @param string $type The type (string) for validator (e.g. 'required'). * @param mixed $value The value for the validator (if applicable) */ protected function getValidatorRule(Column $column, $type, $value = null) { $rule = new Rule(); $rule->setName($type); if ($value !== null) { $rule->setValue($value); } $rule->setMessage($this->getRuleMessage($column, $type, $value)); return $rule; } /** * Gets the message for a specified rule. * * @param Column $column * @param string $type * @param mixed $value */ protected function getRuleMessage(Column $column, $type, $value) { // create message $colName = $column->getName(); $tableName = $column->getTable()->getName(); $msg = self::$validatorMessages[strtolower($type)]; $tmp = compact($msg['var']); array_unshift($tmp, $msg['msg']); $msg = call_user_func_array('sprintf', $tmp); return $msg; } } /** * A helper class to store validator sets indexed by column. * @package propel.generator.task */ class PropelSchemaReverse_ValidatorSet { /** * Map of column names to validators. * * @var array Validator[] */ private $validators = array(); /** * Gets a single validator for specified column name. * @param Column $column * @return Validator */ public function getValidator(Column $column) { $key = $column->getName(); if (!isset($this->validators[$key])) { $this->validators[$key] = new Validator(); $this->validators[$key]->setColumn($column); } return $this->validators[$key]; } /** * Gets all validators. * @return array Validator[] */ public function getValidators() { return $this->validators; } }