From d9975c36a348e93de950036418f270d80fe5db27 Mon Sep 17 00:00:00 2001 From: romanb Date: Sun, 20 Jul 2008 20:13:24 +0000 Subject: [PATCH] Checkin of occasional work from the past weeks. --- lib/Doctrine/Association.php | 228 ++++ lib/Doctrine/Association/ManyToMany.php | 80 ++ lib/Doctrine/Association/OneToMany.php | 28 + lib/Doctrine/Association/OneToOne.php | 139 +++ lib/Doctrine/ClassMetadata.php | 935 ++++++--------- lib/Doctrine/ClassMetadata/Factory.php | 69 +- lib/Doctrine/Collection.php | 176 ++- lib/Doctrine/Connection.php | 96 +- lib/Doctrine/Connection/UnitOfWork.php | 1027 ++++++++++------- lib/Doctrine/Entity.php | 180 +-- lib/Doctrine/EntityManager.php | 42 +- ...{EventListener.php => EventSubscriber.php} | 11 +- lib/Doctrine/Exception.php | 3 +- lib/Doctrine/Hydrator/RecordDriver.php | 2 +- lib/Doctrine/HydratorNew.php | 18 +- .../Internal/CommitOrderCalculator.php | 110 ++ lib/Doctrine/Internal/CommitOrderNode.php | 136 +++ lib/Doctrine/MappingException.php | 16 + lib/Doctrine/Overloadable.php | 2 +- lib/Doctrine/Tree.php | 1 + lib/Doctrine/Validator.php | 1 + tests/Orm/Associations/CascadeTest.php | 74 ++ .../Orm/Associations/OneToOneMappingTest.php | 36 + tests/Orm/Component/CollectionTest.php | 8 +- tests/Orm/Entity/AccessorTest.php | 34 +- tests/Orm/Entity/ConstructorTest.php | 15 +- tests/Orm/UnitOfWorkTest.php | 139 ++- tests/lib/mocks/Doctrine_ConnectionMock.php | 20 + .../lib/mocks/Doctrine_EntityManagerMock.php | 25 + .../mocks/Doctrine_EntityPersisterMock.php | 56 + tests/lib/mocks/Doctrine_SequenceMock.php | 37 + tests/models/cms/CmsArticle.php | 45 +- tests/models/cms/CmsComment.php | 41 +- tests/models/cms/CmsPhonenumber.php | 22 +- tests/models/cms/CmsUser.php | 48 +- tests/models/forum/ForumAdministrator.php | 9 +- tests/models/forum/ForumBoard.php | 26 +- tests/models/forum/ForumCategory.php | 28 +- tests/models/forum/ForumEntry.php | 32 + tests/models/forum/ForumUser.php | 36 +- 40 files changed, 2621 insertions(+), 1410 deletions(-) create mode 100644 lib/Doctrine/Association.php create mode 100644 lib/Doctrine/Association/ManyToMany.php create mode 100644 lib/Doctrine/Association/OneToMany.php create mode 100644 lib/Doctrine/Association/OneToOne.php rename lib/Doctrine/{EventListener.php => EventSubscriber.php} (76%) create mode 100644 lib/Doctrine/Internal/CommitOrderCalculator.php create mode 100644 lib/Doctrine/Internal/CommitOrderNode.php create mode 100644 tests/Orm/Associations/CascadeTest.php create mode 100644 tests/Orm/Associations/OneToOneMappingTest.php create mode 100644 tests/lib/mocks/Doctrine_ConnectionMock.php create mode 100644 tests/lib/mocks/Doctrine_EntityManagerMock.php create mode 100644 tests/lib/mocks/Doctrine_EntityPersisterMock.php create mode 100644 tests/lib/mocks/Doctrine_SequenceMock.php create mode 100644 tests/models/forum/ForumEntry.php diff --git a/lib/Doctrine/Association.php b/lib/Doctrine/Association.php new file mode 100644 index 000000000..df98efc60 --- /dev/null +++ b/lib/Doctrine/Association.php @@ -0,0 +1,228 @@ +. + */ + +#namespace Doctrine::ORM::Mapping; + +/** + * Base class for association mappings. + * + * @since 2.0 + * @author Roman Borschel + * @todo Rename to AssociationMapping. + */ +class Doctrine_Association implements Serializable +{ + const FETCH_MANUAL = 1; + const FETCH_LAZY = 2; + const FETCH_EAGER = 3; + + /** + * Cascade types enumeration. + * + * @var array + */ + protected static $_cascadeTypes = array( + 'all', + 'none', + 'save', + 'delete', + 'refresh' + ); + + protected $_cascades = array(); + protected $_isCascadeDelete; + protected $_isCascadeSave; + protected $_isCascadeRefresh; + + /** + * The fetch mode used for the association. + * + * @var integer + */ + protected $_fetchMode = self::FETCH_MANUAL; + + /** + * Flag that indicates whether the class that defines this mapping is + * the owning side of the association. + * + * @var boolean + */ + protected $_isOwningSide = true; + + /** + * The name of the source Entity (the Entity that defines this mapping). + * + * @var string + */ + protected $_sourceEntityName; + + /** + * The name of the target Entity (the Enitity that is the target of the + * association). + * + * @var unknown_type + */ + protected $_targetEntityName; + + /** + * Identifies the field on the source class (the class this AssociationMapping + * belongs to) that represents the association. + * + * @var string + */ + protected $_sourceFieldName; + + /** + * Identifies the field on the owning side that has the mapping for the + * association. + * + * @var string + */ + protected $_mappedByFieldName; + + /** + * Constructor. + * Creates a new AssociationMapping. + * + * @param array $mapping The mapping definition. + */ + public function __construct(array $mapping) + { + $this->_validateMapping($mapping); + if ($this->_isOwningSide) { + $this->_sourceEntityName = $mapping['sourceEntity']; + $this->_targetEntityName = $mapping['targetEntity']; + $this->_sourceFieldName = $mapping['fieldName']; + } else { + $this->_mappedByFieldName = $mapping['mappedBy']; + } + } + + /** + * Validates & completes the mapping. Mapping defaults are applied here. + * + * @param array $mapping + * @return array The validated & completed mapping. + */ + protected function _validateMapping(array $mapping) + { + if (isset($mapping['mappedBy'])) { + $this->_isOwningSide = false; + } + + if ($this->_isOwningSide) { + if ( ! isset($mapping['targetEntity'])) { + throw Doctrine_MappingException::missingTargetEntity(); + } else if ( ! isset($mapping['fieldName'])) { + throw Doctrine_MappingException::missingFieldName(); + } + } + + return $mapping; + } + + public function isCascadeDelete() + { + if (is_null($this->_isCascadeDelete)) { + $this->_isCascadeDelete = in_array('delete', $this->_cascades); + } + return $this->_isCascadeDelete; + } + + public function isCascadeSave() + { + if (is_null($this->_isCascadeSave)) { + $this->_isCascadeSave = in_array('save', $this->_cascades); + } + return $this->_isCascadeSave; + } + + public function isCascadeRefresh() + { + if (is_null($this->_isCascadeRefresh)) { + $this->_isCascadeRefresh = in_array('refresh', $this->_cascades); + } + return $this->_isCascadeRefresh; + } + + public function isEagerlyFetched() + { + return $this->_fetchMode == self::FETCH_EAGER; + } + + public function isLazilyFetched() + { + return $this->_fetchMode == self::FETCH_LAZY; + } + + public function isManuallyFetched() + { + return $this->_fetchMode == self::FETCH_MANUAL; + } + + public function isOwningSide() + { + return $this->_isOwningSide; + } + + public function isInverseSide() + { + return ! $this->_isOwningSide; + } + + public function getSourceEntityName() + { + return $this->_sourceEntityName; + } + + public function getTargetEntityName() + { + return $this->_targetEntityName; + } + + /** + * Get the name of the field the association is mapped into. + * + */ + public function getSourceFieldName() + { + return $this->_sourceFieldName; + } + + public function getMappedByFieldName() + { + return $this->_mappedByFieldName; + } + + /* Serializable implementation */ + + public function serialize() + { + return ""; + } + + public function unserialize($serialized) + { + return true; + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/Association/ManyToMany.php b/lib/Doctrine/Association/ManyToMany.php new file mode 100644 index 000000000..a52a06a93 --- /dev/null +++ b/lib/Doctrine/Association/ManyToMany.php @@ -0,0 +1,80 @@ +_relationTableName; + } + + /** + * Gets the name of the association class. + * + * @return string + */ + public function getAssociationClassName() + { + return $this->_associationClassName; + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/Association/OneToMany.php b/lib/Doctrine/Association/OneToMany.php new file mode 100644 index 000000000..f9b5677a4 --- /dev/null +++ b/lib/Doctrine/Association/OneToMany.php @@ -0,0 +1,28 @@ +_isCascadeDeleteOrphan; + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/Association/OneToOne.php b/lib/Doctrine/Association/OneToOne.php new file mode 100644 index 000000000..851375c60 --- /dev/null +++ b/lib/Doctrine/Association/OneToOne.php @@ -0,0 +1,139 @@ +. + */ + +#namespace Doctrine::ORM::Mappings; + +#use Doctrine::ORM::Entity; + +/** + * A one-to-one mapping describes a uni-directional mapping from one entity + * to another entity. + * + * @since 2.0 + * @author Roman Borschel + * @todo Rename to OneToOneMapping + */ +class Doctrine_Association_OneToOne extends Doctrine_Association +{ + /** + * Maps the source foreign/primary key fields to the target primary/foreign key fields. + * i.e. source.id (pk) => target.user_id (fk). + * Reverse mapping of _targetToSourceKeyFields. + */ + protected $_sourceToTargetKeyColumns = array(); + + /** + * Maps the target primary/foreign key fields to the source foreign/primary key fields. + * i.e. target.user_id (fk) => source.id (pk). + * Reverse mapping of _sourceToTargetKeyFields. + */ + protected $_targetToSourceKeyColumns = array(); + + /** + * Constructor. + * Creates a new OneToOneMapping. + * + * @param array $mapping The mapping info. + */ + public function __construct(array $mapping) + { + parent::__construct($mapping); + if ($this->isOwningSide()) { + $this->_sourceToTargetKeyColumns = $mapping['joinColumns']; + $this->_targetToSourceKeyColumns = array_flip($this->_sourceToTargetKeyColumns); + } + } + + /** + * Validates & completes the mapping. Mapping defaults are applied here. + * + * @param array $mapping The mapping to validate & complete. + * @return array The validated & completed mapping. + * @override + */ + protected function _validateMapping(array $mapping) + { + $mapping = parent::_validateMapping($mapping); + + if ($this->isOwningSide()) { + if ( ! isset($mapping['joinColumns'])) { + throw Doctrine_MappingException::missingJoinColumns(); + } + } + + return $mapping; + } + + /** + * Gets the source-to-target key column mapping. + * + * @return unknown + */ + public function getSourceToTargetKeyColumns() + { + return $this->_sourceToTargetKeyColumns; + } + + /** + * Gets the target-to-source key column mapping. + * + * @return unknown + */ + public function getTargetToSourceKeyColumns() + { + return $this->_targetToSourceKeyColumns; + } + + /** + * Lazy-loads the associated entity for a given entity. + * + * @param Doctrine::ORM::Entity $entity + * @return void + */ + public function lazyLoadFor(Doctrine_Entity $entity) + { + if ($entity->getClassName() != $this->_sourceClass->getClassName()) { + //error? + } + + $dql = 'SELECT t.* FROM ' . $this->_targetClass->getClassName() . ' t WHERE '; + $params = array(); + foreach ($this->_sourceToTargetKeyFields as $sourceKeyField => $targetKeyField) { + if ($params) { + $dql .= " AND "; + } + $dql .= "t.$targetKeyField = ?"; + $params[] = $entity->_rawGetField($sourceKeyField); + } + + $otherEntity = $this->_targetClass->getEntityManager() + ->query($dql, $params) + ->getFirst(); + + if ( ! $otherEntity) { + $otherEntity = Doctrine_Null::$INSTANCE; + } + $entity->_rawSetReference($this->_sourceFieldName, $otherEntity); + } + +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/ClassMetadata.php b/lib/Doctrine/ClassMetadata.php index 38ce5e425..951e65abc 100644 --- a/lib/Doctrine/ClassMetadata.php +++ b/lib/Doctrine/ClassMetadata.php @@ -21,6 +21,8 @@ #namespace Doctrine::ORM::Mapping; +#use Doctrine::ORM::EntityManager; + /** * A ClassMetadata instance holds all the information (metadata) of an entity and * it's associations and how they're mapped to the relational database. @@ -28,11 +30,50 @@ * * @author Roman Borschel * @since 2.0 + * @todo Rename to ClassDescriptor? */ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable { + /* The inheritance mapping types */ + const INHERITANCE_TYPE_NONE = 'none'; + const INHERITANCE_TYPE_JOINED = 'joined'; + const INHERITANCE_TYPE_SINGLE_TABLE = 'singleTable'; + const INHERITANCE_TYPE_TABLE_PER_CLASS = 'tablePerClass'; + /** - * The name of the entity class that is mapped to the database with this metadata. + * Inheritance types enumeration. + * + * @var array + */ + protected static $_inheritanceTypes = array( + self::INHERITANCE_TYPE_NONE, + self::INHERITANCE_TYPE_JOINED, + self::INHERITANCE_TYPE_SINGLE_TABLE, + self::INHERITANCE_TYPE_TABLE_PER_CLASS + ); + + /* The Id generator types. TODO: belongs more in a DBAL class */ + const GENERATOR_TYPE_AUTO = 'auto'; + const GENERATOR_TYPE_SEQUENCE = 'sequence'; + const GENERATOR_TYPE_TABLE = 'table'; + const GENERATOR_TYPE_IDENTITY = 'identity'; + const GENERATOR_TYPE_NONE = 'none'; + + /** + * Id Generator types enumeration. + * + * @var array + */ + protected static $_generatorTypes = array( + self::GENERATOR_TYPE_AUTO, + self::GENERATOR_TYPE_SEQUENCE, + self::GENERATOR_TYPE_TABLE, + self::GENERATOR_TYPE_IDENTITY, + self::GENERATOR_TYPE_NONE + ); + + /** + * The name of the entity class. * * @var string */ @@ -41,7 +82,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable /** * The name of the entity class that is at the root of the entity inheritance * hierarchy. If the entity is not part of an inheritance hierarchy this is the same - * as the $_entityName. + * as $_entityName. * * @var string */ @@ -56,18 +97,23 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable protected $_customRepositoryClassName; /** - * - * @var Doctrine_Connection + * The EntityManager. + * + * @var Doctrine::ORM::EntityManager */ protected $_em; /** * The names of the parent classes (ancestors). + * + * @var array */ protected $_parentClasses = array(); /** - * The names of all subclasses + * The names of all subclasses. + * + * @var array */ protected $_subClasses = array(); @@ -78,22 +124,20 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @var array */ protected $_identifier = array(); - - /** - * The identifier type of the class. - * - * @see Doctrine::IDENTIFIER_* constants - * @var integer - */ - protected $_identifierType; - + /** * The inheritance mapping type used by the class. * - * * @var integer */ - protected $_inheritanceType = Doctrine::INHERITANCE_TYPE_NONE; + protected $_inheritanceType = self::INHERITANCE_TYPE_NONE; + + /** + * The Id generator type used by the class. + * + * @var string + */ + protected $_generatorType = self::GENERATOR_TYPE_NONE; /** * An array containing all behaviors attached to the class. @@ -102,30 +146,51 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @var array $_templates * @todo Unify under 'Behaviors'. */ - protected $_behaviors = array(); - - /** - * An array containing all behavior generators attached to the class. - * - * @see Doctrine_Record_Generator - * @var array $_generators - * @todo Unify under 'Behaviors'. - */ - protected $_generators = array(); - + //protected $_behaviors = array(); + /** * The field mappings of the class. * Keys are field names and values are mapping definitions. * - * The mapping definition array has at least the following values: - * - * -- type the column type, eg. 'integer' - * -- length the column length, eg. 11 - * - * additional keys: - * -- notnull whether or not the column is marked as notnull - * -- values enum values - * ... many more + * The mapping definition array has the following values: + * + * - fieldName (string) + * The name of the field in the Entity. + * + * - type (string) + * The database type of the column. Can be one of Doctrine's portable types. + * + * - columnName (string, optional) + * The column name. Optional. Defaults to the field name. + * + * - length (integer, optional) + * The database length of the column. Optional. Defaults to Doctrine's + * default values for the portable types. + * + * - id (boolean, optional) + * Marks the field as the primary key of the Entity. Multiple fields of an + * entity can have the id attribute, forming a composite key. + * + * - generatorType (string, optional, requires: id) + * The generation type used for the field. Optional. Can only be applied on + * fields that are marked with the 'id' attribute. The possible values are: + * auto, identity, sequence, table. + * + * - generator (array, optional, requires: generationType=table|sequence) + * The generator options for a table or sequence generator. Can only be applied + * on fields that have a generationType of 'table' or 'sequence'. + * + * - nullable (boolean, optional) + * Whether the column is nullable. Defaults to TRUE. + * + * - columnDefinition (string, optional) + * The SQL fragment that is used when generating the DDL for the column. + * + * - precision (integer, optional) + * The precision of a decimal column. Only valid if the column type is decimal. + * + * - scale (integer, optional) + * The scale of a decimal column. Only valid if the column type is decimal. * * @var array */ @@ -137,7 +202,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @var array * @TODO Implementation (Value Object support) */ - protected $_mappedEmbeddedValues = array(); + //protected $_embeddedValueMappings = array(); /** * Enter description here... @@ -180,62 +245,14 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable */ //protected $_subclassFieldNames = array(); - /** - * Caches enum value mappings. Keys are field names and values arrays with the - * mapping. - */ - protected $_enumValues = array(); - - /** - * Tree object associated with the class. - * - * @var Doctrine_Tree - * @todo Belongs to the NestedSet Behavior plugin. - */ - protected $_tree; - - /** - * Cached column count, Doctrine_Entity uses this column count when - * determining its state. - * - * @var integer - */ - //protected $_columnCount; - - /** - * Whether or not this class has default values. - * - * @var boolean - */ - protected $_hasDefaultValues; - /** * Relation parser object. Manages the relations for the class. * * @var Doctrine_Relation_Parser $_parser + * @todo Remove. */ protected $_parser; - /** - * Enum value arrays. - */ - protected $_enumMap = array(); - - /** - * @var array $options an array containing all options - * - * -- treeImpl the tree implementation of this table (if any) - * - * -- treeOptions the tree options - * - * -- queryParts the bound query parts - */ - protected $_options = array( - 'treeImpl' => null, - 'treeOptions' => null, - 'queryParts' => array() - ); - /** * Inheritance options. */ @@ -276,11 +293,6 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable 'indexes' => array(), 'checks' => array() ); - - /** - * @var array $_invokedMethods method invoker cache - */ - protected $_invokedMethods = array(); /** * The cached lifecycle listeners. There is only one instance of each @@ -304,6 +316,19 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable */ protected $_lifecycleListeners = array(); + /** + * The association mappings. + * + * @var array + */ + protected $_associationMappings = array(); + + /** + * Flag indicating whether the identifier/primary key of the class is composite. + * + * @var boolean + */ + protected $_isIdentifierComposite = false; /** * Constructs a new ClassMetadata instance. @@ -315,11 +340,12 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable $this->_entityName = $entityName; $this->_rootEntityName = $entityName; $this->_em = $em; + $this->_parser = new Doctrine_Relation_Parser($this); } /** - * + * @deprecated */ public function getConnection() { @@ -370,21 +396,21 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable */ public function isIdentifier($fieldName) { - if ($this->_identifierType != Doctrine::IDENTIFIER_COMPOSITE) { + if ( ! $this->_isIdentifierComposite) { return $fieldName === $this->_identifier[0]; } return in_array($fieldName, $this->_identifier); } /** - * Check if the field is unique + * Check if the class has a composite identifier. * * @param string $fieldName The field name - * @return boolean TRUE if the field is unique, FALSE otherwise. + * @return boolean TRUE if the identifier is composite, FALSE otherwise. */ public function isIdentifierComposite() { - return $this->_identifierType == Doctrine::IDENTIFIER_COMPOSITE; + return $this->_isIdentifierComposite; } /** @@ -490,7 +516,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable return $this->_tableOptions[$name]; } - public function getBehaviorForMethod($method) + /*public function getBehaviorForMethod($method) { return (isset($this->_invokedMethods[$method])) ? $this->_invokedMethods[$method] : false; @@ -498,10 +524,9 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable public function addBehaviorMethod($method, $behavior) { $this->_invokedMethods[$method] = $class; - } + }*/ /** - * getOption * returns the value of given option * * @param string $name the name of the option @@ -562,8 +587,11 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable */ public function getFieldMapping($fieldName) { - return isset($this->_fieldMappings[$fieldName]) ? - $this->_fieldMappings[$fieldName] : false; + if ( ! isset($this->_fieldMappings[$fieldName])) { + throw Doctrine_MappingException::mappingNotFound($fieldName); + } + + return $this->_fieldMappings[$fieldName]; } /** @@ -575,7 +603,21 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable */ public function getAssociationMapping($fieldName) { - //... + if ( ! isset($this->_associationMappings[$fieldName])) { + throw Doctrine_MappingException::mappingNotFound($fieldName); + } + + return $this->_associationMappings[$fieldName]; + } + + /** + * Gets all association mappings of the class. + * + * @return array + */ + public function getAssociationMappings() + { + return $this->_associationMappings; } /** @@ -626,7 +668,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * during hydration because the hydrator caches effectively. * * @return string The field name. - * @throws Doctrine::ORM::Exceptions::ClassMetadataException if the field name could + * @throws Doctrine::ORM::Exceptions::ClassMetadataException If the field name could * not be found. */ public function lookupFieldName($lcColumnName) @@ -649,90 +691,95 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable throw new Doctrine_ClassMetadata_Exception("No field name found for column name '$lcColumnName' during lookup."); } - + /** - * Maps a field of the class to a database column. + * Adds a field mapping. * - * @param string $name The name of the column to map. Syntax: columnName [as propertyName]. - * The property name is optional. If not used the column will be - * mapped to a property with the same name. - * @param string $type The type of the column. - * @param integer $length The length of the column. - * @param mixed $options - * @param boolean $prepend Whether to prepend or append the new column to the column list. - * By default the column gets appended. - * - * @throws Doctrine_ClassMetadata_Exception If trying use wrongly typed parameter. - * @todo Rename to mapField()/addFieldMapping(). + * @param array $mapping */ - public function mapColumn($name, $type, $length = null, $options = array()) + public function mapField(array $mapping) { - // converts 0 => 'primary' to 'primary' => true etc. - foreach ($options as $k => $option) { - if (is_numeric($k)) { - if ( ! empty($option) && $option !== false) { - $options[$option] = true; - } - unset($options[$k]); - } + $mapping = $this->_validateAndCompleteFieldMapping($mapping); + if (isset($this->_fieldMappings[$mapping['fieldName']])) { + throw Doctrine_MappingException::duplicateFieldMapping(); } - - // extract column name & field name & lowercased column name - $parts = explode(' as ', $name); - if (count($parts) > 1) { - $fieldName = $parts[1]; - } else { - $fieldName = $parts[0]; - } - $columnName = $parts[0]; - $lcColumnName = strtolower($parts[0]); - - if (isset($this->_fieldMappings[$fieldName])) { - return; - } - - // Fill column name <-> field name lookup maps - $this->_columnNames[$fieldName] = $columnName; - $this->_fieldNames[$columnName] = $fieldName; - $this->_lcColumnToFieldNames[$lcColumnName] = $fieldName; - $this->_lcColumnToFieldNames[$lcColumnName] = $fieldName; - - - // Inspect & fill $options - - if ($length == null) { - $length = $this->_getDefaultLength($type); - } - - $options['type'] = $type; - $options['length'] = $length; - - if ( ! $this->_hasDefaultValues && isset($options['default'])) { - $this->_hasDefaultValues = true; - } - if ( ! empty($options['primary'])) { - if ( ! in_array($fieldName, $this->_identifier)) { - $this->_identifier[] = $fieldName; - } - /*if (isset($options['autoincrement']) && $options['autoincrement'] === true) { - - }*/ - } - /* - if ( ! isset($options['immutable'])) { - $options['immutable'] = false; - }*/ - - $this->_fieldMappings[$fieldName] = $options; - - $this->_columnCount++; + $this->_fieldMappings[$mapping['fieldName']] = $mapping; } /** - * Gets the default length for a field type. + * Overrides an existant field mapping. + * Used i.e. by Entity classes deriving from another Entity class that acts + * as a mapped superclass to refine the basic mapping. * - * @param unknown_type $type - * @return unknown + * @param array $newMapping + * @todo Implementation. + */ + public function overrideFieldMapping(array $newMapping) + { + //... + } + + /** + * Validates & completes the field mapping. Default values are applied here. + * + * @param array $mapping + * @return array + */ + private function _validateAndCompleteFieldMapping(array $mapping) + { + // Check mandatory fields + if ( ! isset($mapping['fieldName'])) { + throw Doctrine_MappingException::missingFieldName(); + } + if ( ! isset($mapping['type'])) { + throw Doctrine_MappingException::missingType(); + } + + // Complete fieldName and columnName mapping + if ( ! isset($mapping['columnName'])) { + $mapping['columnName'] = $mapping['fieldName']; + } + $lcColumnName = strtolower($mapping['columnName']); + + $this->_columnNames[$mapping['fieldName']] = $mapping['columnName']; + $this->_fieldNames[$mapping['columnName']] = $mapping['fieldName']; + $this->_lcColumnToFieldNames[$lcColumnName] = $mapping['fieldName']; + + // Complete length mapping + if ( ! isset($mapping['length'])) { + $mapping['length'] = $this->_getDefaultLength($mapping['type']); + } + + // Complete id mapping + if (isset($mapping['id']) && $mapping['id'] === true) { + if ( ! in_array($mapping['fieldName'], $this->_identifier)) { + $this->_identifier[] = $mapping['fieldName']; + } + if (isset($mapping['generatorType'])) { + if ( ! in_array($mapping['generatorType'], self::$_generatorTypes)) { + throw Doctrine_MappingException::invalidGeneratorType($mapping['generatorType']); + } else if (count($this->_identifier) > 1) { + throw Doctrine_MappingException::generatorNotAllowedWithCompositeId(); + } + $this->_generatorType = $mapping['generatorType']; + } + // TODO: validate/complete 'generator' mapping + + // Check for composite key + if ( ! $this->_isIdentifierComposite && count($this->_identifier) > 1) { + $this->_isIdentifierComposite = true; + } + + } + + return $mapping; + } + + /** + * Gets the default length for a column type. + * + * @param string $type + * @return mixed */ private function _getDefaultLength($type) { @@ -777,39 +824,10 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @param string The field name. * @return array The names of all validators that are applied on the specified field. */ - public function getFieldValidators($fieldName) + /*public function getFieldValidators($fieldName) { return isset($this->_fieldMappings[$fieldName]['validators']) ? $this->_fieldMappings[$fieldName]['validators'] : array(); - } - - /** - * Checks whether the mapped class has a default value on any field. - * - * @return boolean TRUE if the entity has a default value on any field, otherwise false. - */ - /*public function hasDefaultValues() - { - return $this->_hasDefaultValues; - }*/ - - /** - * getDefaultValueOf - * returns the default value(if any) for given field - * - * @param string $fieldName - * @return mixed - */ - /*public function getDefaultValueOf($fieldName) - { - if ( ! isset($this->_fieldMappings[$fieldName])) { - throw new Doctrine_Table_Exception("Couldn't get default value. Column ".$fieldName." doesn't exist."); - } - if (isset($this->_fieldMappings[$fieldName]['default'])) { - return $this->_fieldMappings[$fieldName]['default']; - } else { - return null; - } }*/ /** @@ -832,6 +850,21 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable { return $this->_identifier; } + + /** + * Gets the name of the single id field. Note that this only works on + * entity classes that have a single-field pk. + * + * @return string + */ + public function getSingleIdentifierFieldName() + { + if ($this->_isIdentifierComposite) { + throw new Doctrine_Exception("Calling getSingleIdentifierFieldName " + . "on a class that uses a composite identifier is not allowed."); + } + return $this->_identifier[0]; + } public function setIdentifier(array $identifier) { @@ -839,35 +872,8 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable } /** - * Gets the type of the identifier (primary key) used by the mapped class. The type - * can be either - * Doctrine::IDENTIFIER_NATURAL, - * Doctrine::IDENTIFIER_AUTOINCREMENT, - * Doctrine::IDENTIFIER_SEQUENCE or - * Doctrine::IDENTIFIER_COMPOSITE. - * - * @return integer - */ - public function getIdentifierType() - { - return $this->_identifierType; - } - - /** - * Sets the identifier type used by the mapped class. - */ - public function setIdentifierType($type) - { - $this->_identifierType = $type; - } - - public function hasMappedColumn($columnName) - { - return isset($this->_fieldNames[$columnName]); - } - - /** - * hasField + * Checks whether the class has a (mapped) field with a certain name. + * * @return boolean */ public function hasField($fieldName) @@ -875,87 +881,6 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable return isset($this->_columnNames[$fieldName]); } - /** - * @param string $fieldName - * @return array - */ - /*public function getEnumValues($fieldName) - { - if (isset($this->_fieldMappings[$fieldName]['values'])) { - return $this->_fieldMappings[$fieldName]['values']; - } else { - return array(); - } - }*/ - - /** - * enumValue - * - * @param string $field - * @param integer $index - * @return mixed - */ - /*public function enumValue($fieldName, $index) - { - if ($index instanceof Doctrine_Null) { - return $index; - } - - if (isset($this->_enumValues[$fieldName][$index])) { - return $this->_enumValues[$fieldName][$index]; - } - - $columnName = $this->getColumnName($fieldName); - if ( ! $this->_em->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM) && - isset($this->_fieldMappings[$fieldName]['values'][$index])) { - $enumValue = $this->_fieldMappings[$fieldName]['values'][$index]; - } else { - $enumValue = $index; - } - $this->_enumValues[$fieldName][$index] = $enumValue; - - return $enumValue; - }*/ - - /** - * enumIndex - * - * @param string $field - * @param mixed $value - * @return mixed - */ - /*public function enumIndex($fieldName, $value) - { - $values = $this->getEnumValues($fieldName); - $index = array_search($value, $values); - if ($index === false || ! $this->_em->getAttribute(Doctrine::ATTR_USE_NATIVE_ENUM)) { - return $index; - } - - return $value; - }*/ - - /** - * getColumnCount - * - * @return integer the number of columns in this table - * @deprecated - */ - /*public function getColumnCount() - { - return $this->_columnCount; - }*/ - - /** - * getMappedColumnCount - * - * @return integer the number of mapped columns in the class. - */ - public function getMappedFieldCount() - { - return $this->_columnCount; - } - /** * Gets the custom accessor of a field. * @@ -983,7 +908,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable /** * Gets all field mappings. * - * @return unknown + * @return array */ public function getFieldMappings() { @@ -991,28 +916,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable } /** - * removeColumn - * removes given column - * - * @return boolean - */ - /*public function removeColumn($fieldName) - { - $columnName = array_search($fieldName, $this->_fieldNames); - - unset($this->_fieldNames[$columnName]); - - if (isset($this->_fieldMappings[$fieldName])) { - unset($this->_fieldMappings[$fieldName]); - return true; - } - $this->_columnCount--; - - return false; - }*/ - - /** - * returns an array containing all the column names. + * Gets an array containing all the column names. * * @return array */ @@ -1024,14 +928,13 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable $columnNames = array(); foreach ($fieldNames as $fieldName) { $columnNames[] = $this->getColumnName($fieldName); - } - + } return $columnNames; } } /** - * returns an array with all the identifier column names. + * Returns an array with all the identifier column names. * * @return array */ @@ -1041,7 +944,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable } /** - * returns an array containing all the field names. + * Returns an array containing all the field names. * * @return array */ @@ -1049,19 +952,67 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable { return array_values($this->_fieldNames); } - + /** - * getDefinitionOf + * Gets the Id generator type used by the class. * - * @return mixed array on success, false on failure - * @deprecated + * @return string */ - /*public function getDefinitionOf($fieldName) + public function getIdGeneratorType() { - $columnName = $this->getColumnName($fieldName); - - return $this->getColumnDefinition($columnName); - }*/ + return $this->_generatorType; + } + + /** + * Checks whether the class uses an Id generator. + * + * @return boolean TRUE if the class uses an Id generator, FALSE otherwise. + */ + public function usesIdGenerator() + { + return $this->_generatorType != self::GENERATOR_TYPE_NONE; + } + + /** + * Checks whether the class uses an identity column for the Id generation. + * + * @return boolean TRUE if the class uses the IDENTITY generator, FALSE otherwise. + */ + public function isIdGeneratorIdentity() + { + return $this->_generatorType == self::GENERATOR_TYPE_IDENTITY; + } + + /** + * Checks whether the class uses a sequence for id generation. + * + * @return boolean TRUE if the class uses the SEQUENCE generator, FALSE otherwise. + */ + public function isIdGeneratorSequence() + { + return $this->_generatorType == self::GENERATOR_TYPE_SEQUENCE; + } + + /** + * Checks whether the class uses a table for id generation. + * + * @return boolean TRUE if the class uses the TABLE generator, FALSE otherwise. + */ + public function isIdGeneratorTable() + { + $this->_generatorType == self::GENERATOR_TYPE_TABLE; + } + + /** + * Checks whether the class has a natural identifier/pk (which means it does + * not use any Id generator. + * + * @return boolean + */ + public function isIdentifierNatural() + { + return $this->_generatorType == self::GENERATOR_TYPE_NONE; + } /** * Gets the type of a field. @@ -1127,10 +1078,10 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @return array an array containing all templates * @todo Unify under 'Behaviors' */ - public function getBehaviors() + /*public function getBehaviors() { return $this->_behaviors; - } + }*/ /** * Gets the inheritance type used by the class. @@ -1186,6 +1137,8 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable /** * Sets the parent class names. + * Assumes that the class names in the passed array are in the order: + * directParent -> directParentParent -> directParentParentParent ... -> root. */ public function setParentClasses(array $classNames) { @@ -1213,20 +1166,17 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable public function setInheritanceType($type, array $options = array()) { if ($parentClassNames = $this->getParentClasses()) { - if ($this->_em->getClassMetadata($parentClassNames[0])->getInheritanceType() != $type) { - throw new Doctrine_ClassMetadata_Exception("All classes in an inheritance hierarchy" - . " must share the same inheritance mapping type. Mixing is not allowed."); - } + throw new Doctrine_MappingException("All classes in an inheritance hierarchy" + . " must share the same inheritance mapping type and this type must be set" + . " in the root class of the hierarchy."); } - - if ($type == Doctrine::INHERITANCE_TYPE_SINGLE_TABLE) { + if ( ! in_array($type, self::$_inheritanceTypes)) { + throw Doctrine_MappingException::invalidInheritanceType($type); + } + + if ($type == Doctrine::INHERITANCE_TYPE_SINGLE_TABLE || + $type == Doctrine::INHERITANCE_TYPE_JOINED) { $this->_checkRequiredDiscriminatorOptions($options); - } else if ($type == Doctrine::INHERITANCE_TYPE_JOINED) { - $this->_checkRequiredDiscriminatorOptions($options); - } else if ($type == Doctrine::INHERITANCE_TYPE_TABLE_PER_CLASS) { - ; - } else { - throw new Doctrine_ClassMetadata_Exception("Invalid inheritance type '$type'."); } $this->_inheritanceType = $type; @@ -1284,19 +1234,24 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable throw new Doctrine_ClassMetadata_Exception("Unknown inheritance option: '$name'."); } - switch ($name) { - case 'discriminatorColumn': - if ($value !== null && ! is_string($value)) { - throw new Doctrine_ClassMetadata_Exception("Invalid value '$value' for option" - . " 'discriminatorColumn'."); - } - break; - case 'discriminatorMap': - if ( ! is_array($value)) { - throw new Doctrine_ClassMetadata_Exception("Value for option 'discriminatorMap'" - . " must be an array."); - } - break; + if ($this->_inheritanceType == 'joined' || $this->_inheritanceType == 'singleTable') { + switch ($name) { + case 'discriminatorColumn': + if ($value !== null && ! is_string($value)) { + throw new Doctrine_ClassMetadata_Exception("Invalid value '$value' for option" + . " 'discriminatorColumn'."); + } + break; + case 'discriminatorMap': + if ( ! is_array($value)) { + throw new Doctrine_ClassMetadata_Exception("Value for option 'discriminatorMap'" + . " must be an array."); + } + break; + // ... further validation checks as needed + default: + throw Doctrine_MappingException::invalidInheritanceOption($name); + } } $this->_inheritanceOptions[$name] = $value; @@ -1438,135 +1393,6 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable 'options' => array_merge($options, $this->getTableOptions())); } - /** - * getTemplate - * - * @param string $template - * @return void - * @todo Unify under 'Behaviors'. - */ - public function getBehavior($behaviorName) - { - if ( ! isset($this->_behaviors[$behaviorName])) { - throw new Doctrine_Table_Exception('Template ' . $behaviorName . ' not loaded'); - } - - return $this->_behaviors[$behaviorName]; - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function hasBehavior($behaviorName) - { - return isset($this->_behaviors[$behaviorName]); - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function addBehavior($behaviorName, Doctrine_Template $impl) - { - $this->_behaviors[$behaviorName] = $impl; - - return $this; - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function getGenerators() - { - return $this->_generators; - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function getGenerator($generator) - { - if ( ! isset($this->_generators[$generator])) { - throw new Doctrine_Table_Exception('Generator ' . $generator . ' not loaded'); - } - - return $this->_generators[$plugin]; - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function hasGenerator($generator) - { - return isset($this->_generators[$generator]); - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function addGenerator(Doctrine_Record_Generator $generator, $name = null) - { - if ($name === null) { - $this->_generators[] = $generator; - } else { - $this->_generators[$name] = $generator; - } - return $this; - } - - /** - * loadBehavior - * - * @param string $template - * @todo Unify under 'Behaviors'. - */ - public function loadBehavior($behavior, array $options = array()) - { - $this->actAs($behavior, $options); - } - - /** - * @todo Unify under 'Behaviors'. - */ - public function loadGenerator(Doctrine_Record_Generator $generator) - { - $generator->initialize($this->_table); - $this->addGenerator($generator, get_class($generator)); - } - - /** - * getTree - * - * getter for associated tree - * - * @return mixed if tree return instance of Doctrine_Tree, otherwise returns false - * @todo Belongs to the NestedSet Behavior. - */ - public function getTree() - { - if ($this->getOption('treeImpl')) { - if ( ! $this->_tree) { - $options = $this->getOption('treeOptions') ? $this->getOption('treeOptions') : array(); - $this->_tree = Doctrine_Tree::factory($this, - $this->getOption('treeImpl'), $options); - } - return $this->_tree; - } - return false; - } - - /** - * isTree - * - * determine if table acts as tree - * - * @return mixed if tree return true, otherwise returns false - * @todo Belongs to the NestedSet Behavior. - */ - public function isTree() - { - return ( ! is_null($this->getOption('treeImpl'))) ? true : false; - } - /** * Checks whether a persistent field is inherited from a superclass. * @@ -1577,47 +1403,6 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable return isset($this->_fieldMappings[$fieldName]['inherited']); } - /** - * bindQueryParts - * binds query parts to given component - * - * @param array $queryParts an array of pre-bound query parts - * @return Doctrine_Entity this object - */ - public function bindQueryParts(array $queryParts) - { - $this->_options['queryParts'] = $queryParts; - return $this; - } - - /** - * bindQueryPart - * binds given value to given query part - * - * @param string $queryPart - * @param mixed $value - * @return Doctrine_Entity this object - */ - public function bindQueryPart($queryPart, $value) - { - $this->_options['queryParts'][$queryPart] = $value; - return $this; - } - - /** - * getBoundQueryPart - * - * @param string $queryPart - * @return string $queryPart - */ - public function getBoundQueryPart($queryPart) - { - if ( ! isset($this->_options['queryParts'][$queryPart])) { - return null; - } - return $this->_options['queryParts'][$queryPart]; - } - /** * Sets the name of the primary table the class is mapped to. * @@ -1657,9 +1442,23 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable } /** + * Adds a one-to-one association mapping. + * * @todo Implementation. */ - public function oneToOne($targetEntity, $definition) + public function mapOneToOne(array $mapping) + { + $oneToOneMapping = new Doctrine_Association_OneToOne($mapping); + if (isset($this->_associationMappings[$oneToOneMapping->getSourceFieldName()])) { + throw Doctrine_MappingException::duplicateFieldMapping(); + } + $this->_associationMappings[$oneToOneMapping->getSourceFieldName()] = $oneToOneMapping; + } + + /** + * @todo Implementation. + */ + public function mapOneToMany(array $mapping) { } @@ -1667,23 +1466,15 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable /** * @todo Implementation. */ - public function oneToMany($targetEntity, $definition) + public function mapManyToOne(array $mapping) { - + } /** * @todo Implementation. */ - public function manyToOne($targetEntity, $definition) - { - - } - - /** - * @todo Implementation. - */ - public function manyToMany($targetEntity, $definition) + public function mapManyToMany(array $mapping) { } @@ -1696,7 +1487,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @param array $options * @todo Unify under 'Behaviors'. */ - public function actAs($tpl, array $options = array()) + /*public function actAs($tpl, array $options = array()) { if ( ! is_object($tpl)) { if (class_exists($tpl, true)) { @@ -1723,7 +1514,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable $tpl->setTableDefinition(); return $this; - } + }*/ /** * check @@ -1735,7 +1526,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable * @todo Should be done through $_tableOptions * @deprecated */ - public function check($constraint, $name = null) + /*public function check($constraint, $name = null) { if (is_array($constraint)) { foreach ($constraint as $name => $def) { @@ -1754,7 +1545,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable } else { $this->_tableOptions['checks'][] = $definition; } - } + }*/ /** * Registers a custom mapper for the entity class. @@ -1868,7 +1659,7 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable /** * Adds a lifecycle callback for Entities of this class. * - * Note: If a the same callback is registered more than once, the old one + * Note: If the same callback is registered more than once, the old one * will be overridden. * * @param string $callback @@ -2015,6 +1806,14 @@ class Doctrine_ClassMetadata implements Doctrine_Configurable, Serializable { return $this->_parser->getRelations(); } + + /** + * @todo Set by the factory if type is AUTO. Not pretty. Find sth. better. + */ + public function setIdGeneratorType($type) + { + $this->_generatorType = $type; + } /** * diff --git a/lib/Doctrine/ClassMetadata/Factory.php b/lib/Doctrine/ClassMetadata/Factory.php index cba11a2af..c11190a30 100644 --- a/lib/Doctrine/ClassMetadata/Factory.php +++ b/lib/Doctrine/ClassMetadata/Factory.php @@ -19,6 +19,8 @@ * . */ +#namespace Doctrine::ORM::Internal; + /** * The metadata factory is used to create ClassMetadata objects that contain all the * metadata of a class. @@ -31,10 +33,11 @@ * @version $Revision$ * @link www.phpdoctrine.org * @since 2.0 + * @todo Rename to ClassMetadataFactory. */ class Doctrine_ClassMetadata_Factory { - protected $_conn; + protected $_em; protected $_driver; /** @@ -52,7 +55,7 @@ class Doctrine_ClassMetadata_Factory */ public function __construct(Doctrine_EntityManager $em, $driver) { - $this->_conn = $em; + $this->_em = $em; $this->_driver = $driver; } @@ -101,7 +104,7 @@ class Doctrine_ClassMetadata_Factory $class = $classes[$loadedParentClass]; } else { $rootClassOfHierarchy = count($parentClasses) > 0 ? array_shift($parentClasses) : $name; - $class = new Doctrine_ClassMetadata($rootClassOfHierarchy, $this->_conn); + $class = new Doctrine_ClassMetadata($rootClassOfHierarchy, $this->_em); $this->_loadMetadata($class, $rootClassOfHierarchy); $classes[$rootClassOfHierarchy] = $class; } @@ -115,7 +118,7 @@ class Doctrine_ClassMetadata_Factory $parent = $class; foreach ($parentClasses as $subclassName) { - $subClass = new Doctrine_ClassMetadata($subclassName, $this->_conn); + $subClass = new Doctrine_ClassMetadata($subclassName, $this->_em); $subClass->setInheritanceType($parent->getInheritanceType(), $parent->getInheritanceOptions()); $this->_addInheritedFields($subClass, $parent); $this->_addInheritedRelations($subClass, $parent); @@ -130,14 +133,10 @@ class Doctrine_ClassMetadata_Factory protected function _addInheritedFields($subClass, $parentClass) { - foreach ($parentClass->getFieldMappings() as $name => $definition) { + foreach ($parentClass->getFieldMappings() as $fieldName => $mapping) { $fullName = "$name as " . $parentClass->getFieldName($name); - $definition['inherited'] = true; - $subClass->mapColumn( - $fullName, - $definition['type'], - $definition['length'], - $definition); + $mapping['inherited'] = true; + $subClass->mapField($mapping); } } @@ -163,6 +162,7 @@ class Doctrine_ClassMetadata_Factory $names = array(); $className = $name; // get parent classes + //TODO: Skip Entity types MappedSuperclass/Transient do { if ($className === 'Doctrine_Entity') { break; @@ -182,11 +182,13 @@ class Doctrine_ClassMetadata_Factory // load further metadata $this->_driver->loadMetadataForClass($name, $class); + // set default table name, if necessary $tableName = $class->getTableName(); if ( ! isset($tableName)) { $class->setTableName(Doctrine::tableize($class->getClassName())); } + // complete identifier mapping $this->_initIdentifier($class); return $class; @@ -199,7 +201,7 @@ class Doctrine_ClassMetadata_Factory */ protected function _initIdentifier(Doctrine_ClassMetadata $class) { - switch (count((array)$class->getIdentifier())) { + /*switch (count($class->getIdentifier())) { case 0: // No identifier in the class mapping yet // If its a subclass, inherit the identifier from the parent. @@ -217,7 +219,7 @@ class Doctrine_ClassMetadata_Factory } // add all inherited primary keys - foreach ((array) $class->getIdentifier() as $id) { + foreach ($class->getIdentifier() as $id) { $definition = $rootClass->getDefinitionOf($id); // inherited primary keys shouldn't contain autoinc @@ -231,16 +233,7 @@ class Doctrine_ClassMetadata_Factory $definition, true); } } else { - throw Doctrine_MappingException::identifierRequired($class->getClassName()); - /* Legacy behavior of auto-adding an id field - $definition = array('type' => 'integer', - 'length' => 20, - 'autoincrement' => true, - 'primary' => true); - $class->mapColumn('id', $definition['type'], $definition['length'], $definition, true); - $class->setIdentifier(array('id')); - $class->setIdentifierType(Doctrine::IDENTIFIER_AUTOINC); - */ + throw Doctrine_MappingException::identifierRequired($class->getClassName()); } break; case 1: // A single identifier is in the mapping @@ -293,6 +286,36 @@ class Doctrine_ClassMetadata_Factory break; default: // Multiple identifiers are in the mapping so its a composite id $class->setIdentifierType(Doctrine::IDENTIFIER_COMPOSITE); + }*/ + + // If the chosen generator type is "auto", then pick the one appropriate for + // the database. + + // FIXME: This is very ugly here. Such switch()es on the database driver + // are unnecessary as we can easily replace them with polymorphic calls on + // the connection (or another) object. We just need to decide where to put + // the id generation types. + if ($class->getIdGeneratorType() == Doctrine_ClassMetadata::GENERATOR_TYPE_AUTO) { + switch (strtolower($this->_em->getConnection()->getDriverName())) { + case 'mysql': + // pick IDENTITY + $class->setIdGeneratorType(Doctrine_ClassMetadata::GENERATOR_TYPE_IDENTITY); + break; + case 'oracle': + //pick SEQUENCE + break; + case 'postgres': + //pick SEQUENCE + break; + case 'firebird': + //pick what? + break; + case 'mssql': + //pick what? + default: + throw new Doctrine_Exception("Encountered unknown database driver: " + . $this->_em->getConnection()->getDriverName()); + } } } diff --git a/lib/Doctrine/Collection.php b/lib/Doctrine/Collection.php index bdb22d39a..8f08ccbd6 100644 --- a/lib/Doctrine/Collection.php +++ b/lib/Doctrine/Collection.php @@ -33,10 +33,10 @@ * @since 1.0 * @version $Revision$ * @author Konsta Vesterinen - * @todo Rename to EntityCollection */ class Doctrine_Collection extends Doctrine_Access implements Countable, IteratorAggregate, Serializable -{ +{ + protected $_entityBaseType; /** @@ -44,42 +44,30 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator * * @var array */ - protected $data = array(); - - /** - * The mapper object used to map the records of this collection to the database. - * - * @var Doctrine_Mapper - */ - protected $_mapper; + protected $_data = array(); /** - * A snapshot of the fetched data. + * A snapshot of the collection at the moment it was fetched from the database. + * This is used to create a diff of the collection at commit time. * - * @var array + * @var array */ protected $_snapshot = array(); /** - * This record this collection is attached to, if any. + * This entity that owns this collection. * - * @var Doctrine_Entity + * @var Doctrine::ORM::Entity */ protected $_owner; /** - * The reference field of the collection. + * The association mapping the collection belongs to. + * This is currently either a OneToManyMapping or a ManyToManyMapping. * - * @var string $referenceField + * @var Doctrine::ORM::Mapping::AssociationMapping */ - protected $referenceField; - - /** - * The relation this collection is related to, if any. - * - * @var Doctrine_Relation - */ - protected $relation; + protected $_associationMapping; /** * The name of the column that is used for collection key mapping. @@ -101,6 +89,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator * @var EntityManager */ protected $_em; + protected $_isDirty = false; /** * Constructor. @@ -113,18 +102,9 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator { $this->_entityBaseType = $entityBaseType; $this->_em = Doctrine_EntityManagerFactory::getManager($entityBaseType); - $this->_mapper = $this->_em->getEntityPersister($entityBaseType); - - if ($keyField === null) { - $keyField = $this->_mapper->getClassMetadata()->getBoundQueryPart('indexBy'); - } - - if ($keyField === null) { - //$keyField = $mapper->getClassMetadata()->getAttribute(Doctrine::ATTR_COLL_KEY); - } if ($keyField !== null) { - if ( ! $this->_mapper->getClassMetadata()->hasField($keyField)) { + if ( ! $this->_em->getClassMetadata($entityBaseType)->hasField($keyField)) { throw new Doctrine_Collection_Exception("Invalid field '$keyField' can't be uses as key."); } $this->_keyField = $keyField; @@ -139,7 +119,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function setData(array $data) { - $this->data = $data; + $this->_data = $data; } /** @@ -155,14 +135,11 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $vars = get_object_vars($this); unset($vars['reference']); - unset($vars['reference_field']); unset($vars['relation']); unset($vars['expandable']); unset($vars['expanded']); unset($vars['generator']); - $vars['_mapper'] = $vars['_mapper']->getComponentName(); - return serialize($vars); } @@ -187,12 +164,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $this->$name = $values; } - $this->_mapper = $manager->getEntityPersister($this->_entityBaseType); - $keyColumn = isset($array['keyField']) ? $array['keyField'] : null; - if ($keyColumn === null) { - $keyColumn = $this->_mapper->getClassMetadata()->getBoundQueryPart('indexBy'); - } if ($keyColumn !== null) { $this->_keyField = $keyColumn; @@ -231,7 +203,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getData() { - return $this->data; + return $this->_data; } /** @@ -242,7 +214,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getFirst() { - return reset($this->data); + return reset($this->_data); } /** @@ -253,7 +225,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getLast() { - return end($this->data); + return end($this->_data); } /** @@ -263,7 +235,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function end() { - return end($this->data); + return end($this->_data); } /** @@ -273,7 +245,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function key() { - return key($this->data); + return key($this->_data); } /** @@ -285,13 +257,13 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator public function setReference(Doctrine_Entity $entity, Doctrine_Relation $relation) { $this->_owner = $entity; - $this->relation = $relation; + //$this->relation = $relation; - if ($relation instanceof Doctrine_Relation_ForeignKey || + /*if ($relation instanceof Doctrine_Relation_ForeignKey || $relation instanceof Doctrine_Relation_LocalKey) { $this->referenceField = $relation->getForeignFieldName(); $value = $entity->get($relation->getLocalFieldName()); - foreach ($this->data as $entity) { + foreach ($this->_data as $entity) { if ($value !== null) { $entity->set($this->referenceField, $value, false); } else { @@ -300,7 +272,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator } } else if ($relation instanceof Doctrine_Relation_Association) { - } + }*/ } /** @@ -322,8 +294,8 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function remove($key) { - $removed = $this->data[$key]; - unset($this->data[$key]); + $removed = $this->_data[$key]; + unset($this->_data[$key]); return $removed; } @@ -336,7 +308,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function contains($key) { - return isset($this->data[$key]); + return isset($this->_data[$key]); } /** @@ -344,7 +316,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function search(Doctrine_Entity $record) { - return array_search($record, $this->data, true); + return array_search($record, $this->_data, true); } /** @@ -357,8 +329,8 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function get($key) { - if (isset($this->data[$key])) { - return $this->data[$key]; + if (isset($this->_data[$key])) { + return $this->_data[$key]; } return null; } @@ -374,7 +346,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $list = array(); $idFieldNames = (array)$this->_mapper->getClassMetadata()->getIdentifier(); - foreach ($this->data as $record) { + foreach ($this->_data as $record) { if (is_array($record)) { if (count($idFieldNames) > 1) { $id = array(); @@ -406,7 +378,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getKeys() { - return array_keys($this->data); + return array_keys($this->_data); } /** @@ -418,7 +390,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function count() { - return count($this->data); + return count($this->_data); } /** @@ -429,69 +401,49 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator * @internal Can't type-hint the second parameter to Doctrine_Entity because we need * to adhere to the Doctrine_Access::set() signature. */ - public function set($key, $entity) + public function set($key, $value) { - if ( ! $entity instanceof Doctrine_Entity) { + if ( ! $value instanceof Doctrine_Entity) { throw new Doctrine_Collection_Exception('Value variable in set is not an instance of Doctrine_Entity'); } - if (isset($this->referenceField)) { - $entity->set($this->referenceField, $this->_owner, false); - } - $this->data[$key] = $entity; + $this->_data[$key] = $value; } /** * adds a record to collection + * * @param Doctrine_Entity $record record to be added * @param string $key optional key for the record * @return boolean */ - public function add($record, $key = null) + public function add($value, $key = null) { /** @TODO Use raw getters/setters */ - if ( ! $record instanceof Doctrine_Entity) { + if ( ! $value instanceof Doctrine_Entity) { throw new Doctrine_Record_Exception('Value variable in set is not an instance of Doctrine_Entity.'); } - - if (isset($this->referenceField)) { - $value = $this->_owner->get($this->relation->getLocalFieldName()); - - if ($value !== null) { - $record->set($this->referenceField, $value, false); - } else { - $record->set($this->referenceField, $this->_owner, false); - } - } + /* * for some weird reason in_array cannot be used here (php bug ?) * * if used it results in fatal error : [ nesting level too deep ] */ - foreach ($this->data as $val) { - if ($val === $record) { + foreach ($this->_data as $val) { + if ($val === $value) { return false; } } if (isset($key)) { - if (isset($this->data[$key])) { + if (isset($this->_data[$key])) { return false; } - $this->data[$key] = $record; + $this->_data[$key] = $value; return true; } - // why is this not checked when the keyColumn is set? - if (isset($this->_keyField)) { - $value = $record->get($this->_keyField); - if ($value === null) { - throw new Doctrine_Collection_Exception("Couldn't create collection index. Record field '".$this->_keyField."' was null."); - } - $this->data[$value] = $record; - } else { - $this->data[] = $record; - } + $this->_data[] = $value; return true; } @@ -509,7 +461,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $query = new Doctrine_Query($this->_mapper->getConnection()); if ( ! isset($name)) { - foreach ($this->data as $record) { + foreach ($this->_data as $record) { // FIXME: composite key support $ids = $record->identifier(); $value = count($ids) > 0 ? array_pop($ids) : null; @@ -528,11 +480,11 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $rel = $this->_mapper->getTable()->getRelation($name); if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) { - foreach ($this->data as $record) { + foreach ($this->_data as $record) { $list[] = $record[$rel->getLocal()]; } } else { - foreach ($this->data as $record) { + foreach ($this->_data as $record) { $ids = $record->identifier(); $value = count($ids) > 0 ? array_pop($ids) : null; if ($value !== null) { @@ -563,15 +515,15 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $local = $rel->getLocal(); if ($rel instanceof Doctrine_Relation_LocalKey) { - foreach ($this->data as $key => $record) { + foreach ($this->_data as $key => $record) { foreach ($coll as $k => $related) { if ($related[$foreign] == $record[$local]) { - $this->data[$key]->_setRelated($name, $related); + $this->_data[$key]->_setRelated($name, $related); } } } } else if ($rel instanceof Doctrine_Relation_ForeignKey) { - foreach ($this->data as $key => $record) { + foreach ($this->_data as $key => $record) { if ( ! $record->exists()) { continue; } @@ -584,7 +536,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator } } - $this->data[$key]->_setRelated($name, $sub); + $this->_data[$key]->_setRelated($name, $sub); } } else if ($rel instanceof Doctrine_Relation_Association) { // @TODO composite key support @@ -592,7 +544,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $asf = $rel->getAssociationFactory(); $name = $table->getComponentName(); - foreach ($this->data as $key => $record) { + foreach ($this->_data as $key => $record) { if ( ! $record->exists()) { continue; } @@ -603,7 +555,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator $sub->add($related->get($name)); } } - $this->data[$key]->_setRelated($name, $sub); + $this->_data[$key]->_setRelated($name, $sub); } } @@ -635,7 +587,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function takeSnapshot() { - $this->_snapshot = $this->data; + $this->_snapshot = $this->_data; return $this; } @@ -664,7 +616,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function processDiff() { - foreach (array_udiff($this->_snapshot, $this->data, array($this, "_compareRecords")) as $record) { + foreach (array_udiff($this->_snapshot, $this->_data, array($this, "_compareRecords")) as $record) { $record->delete(); } return $this; @@ -686,6 +638,11 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator return $data; } + + public function isEmpty() + { + return $this->count() == 0; + } /** * fromArray @@ -776,7 +733,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getDeleteDiff() { - return array_udiff($this->_snapshot, $this->data, array($this, "_compareRecords")); + return array_udiff($this->_snapshot, $this->_data, array($this, "_compareRecords")); } /** @@ -786,7 +743,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getInsertDiff() { - return array_udiff($this->data, $this->_snapshot, array($this, "_compareRecords")); + return array_udiff($this->_data, $this->_snapshot, array($this, "_compareRecords")); } /** @@ -803,7 +760,6 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator } /** - * save * Saves all records of this collection and processes the * difference of the last snapshot and the current data. * @@ -869,7 +825,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator } } - $this->data = array(); + $this->_data = array(); if ($this->_owner) { $this->_owner->free($deep); @@ -884,7 +840,7 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator */ public function getIterator() { - $data = $this->data; + $data = $this->_data; return new ArrayIterator($data); } @@ -907,6 +863,6 @@ class Doctrine_Collection extends Doctrine_Access implements Countable, Iterator public function clear() { - $this->data = array(); + $this->_data = array(); } } diff --git a/lib/Doctrine/Connection.php b/lib/Doctrine/Connection.php index ce091b850..d10b3b00f 100644 --- a/lib/Doctrine/Connection.php +++ b/lib/Doctrine/Connection.php @@ -138,8 +138,9 @@ abstract class Doctrine_Connection implements Countable * @var array $properties */ protected $properties = array( - 'sql_comments' => array(array('start' => '--', 'end' => "\n", 'escape' => false), - array('start' => '/*', 'end' => '*/', 'escape' => false)), + 'sql_comments' => array( + array('start' => '--', 'end' => "\n", 'escape' => false), + array('start' => '/*', 'end' => '*/', 'escape' => false)), 'identifier_quoting' => array('start' => '"', 'end' => '"','escape' => '"'), 'string_quoting' => array('start' => "'", 'end' => "'", 'escape' => false, 'escape_pattern' => false), @@ -154,6 +155,8 @@ abstract class Doctrine_Connection implements Countable /** * The parameters used during creation of the Connection. + * + * @var array */ protected $_params = array(); @@ -364,24 +367,24 @@ abstract class Doctrine_Connection implements Countable //$this->getListener()->preConnect($event); // TODO: the extension_loaded check can happen earlier, maybe in the factory - if (extension_loaded('pdo')) { - $driverOptions = isset($this->_params['driverOptions']) ? - $this->_params['driverOptions'] : array(); - $user = isset($this->_params['user']) ? - $this->_params['user'] : null; - $password = isset($this->_params['password']) ? - $this->_params['password'] : null; - $this->_pdo = new PDO( - $this->_constructPdoDsn(), - $user, - $password, - $driverOptions - ); - $this->_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->_pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); - } else { + if ( ! extension_loaded('pdo')) { throw new Doctrine_Connection_Exception("Couldn't locate driver named " . $e[0]); } + + $driverOptions = isset($this->_params['driverOptions']) ? + $this->_params['driverOptions'] : array(); + $user = isset($this->_params['user']) ? + $this->_params['user'] : null; + $password = isset($this->_params['password']) ? + $this->_params['password'] : null; + $this->_pdo = new PDO( + $this->_constructPdoDsn(), + $user, + $password, + $driverOptions + ); + $this->_pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->_pdo->setAttribute(PDO::ATTR_CASE, PDO::CASE_LOWER); // attach the pending attributes to adapter /*foreach($this->pendingAttributes as $attr => $value) { @@ -427,9 +430,8 @@ abstract class Doctrine_Connection implements Countable */ public function supports($feature) { - return (isset($this->supported[$feature]) - && ($this->supported[$feature] === 'emulated' - || $this->supported[$feature])); + return (isset($this->supported[$feature]) && + ($this->supported[$feature] === 'emulated' || $this->supported[$feature])); } /** @@ -662,8 +664,7 @@ abstract class Doctrine_Connection implements Countable } /** - * quote - * quotes given input parameter + * Quotes given input parameter * * @param mixed $input parameter to be quoted * @param string $type @@ -794,10 +795,9 @@ abstract class Doctrine_Connection implements Countable $this->getAttribute(Doctrine::ATTR_LISTENER)->postPrepare($event); return new Doctrine_Connection_Statement($this, $stmt); - } catch(Doctrine_Adapter_Exception $e) { - } catch(PDOException $e) { } - - $this->rethrowException($e, $this); + } catch (PDOException $e) { + $this->rethrowException($e, $this); + } } /** @@ -859,10 +859,9 @@ abstract class Doctrine_Connection implements Countable return $stmt; } - } catch (Doctrine_Adapter_Exception $e) { - } catch (PDOException $e) { } - - $this->rethrowException($e, $this); + } catch (PDOException $e) { + $this->rethrowException($e, $this); + } } /** @@ -894,10 +893,9 @@ abstract class Doctrine_Connection implements Countable return $count; } - } catch (Doctrine_Adapter_Exception $e) { - } catch (PDOException $e) { } - - $this->rethrowException($e, $this); + } catch (PDOException $e) { + $this->rethrowException($e, $this); + } } /** @@ -923,7 +921,7 @@ abstract class Doctrine_Connection implements Countable } /** - * rethrowException + * Wraps the given exception into a driver-specific exception and rethrows it. * * @throws Doctrine_Connection_Exception */ @@ -940,9 +938,7 @@ abstract class Doctrine_Connection implements Countable } $exc->processErrorInfo($e->errorInfo); - if ($this->getAttribute(Doctrine::ATTR_THROW_EXCEPTIONS)) { - throw $exc; - } + throw $exc; //$this->getListener()->postError($event); } @@ -1178,7 +1174,6 @@ abstract class Doctrine_Connection implements Countable } } - public function getFormatter() { if ( ! $this->modules['formatter']) { @@ -1186,4 +1181,25 @@ abstract class Doctrine_Connection implements Countable } return $this->modules['formatter']; } + + public function getSequenceModule() + { + if ( ! $this->modules['sequence']) { + $class = "Doctrine_Sequence_" . $this->_driverName; + $this->modules['sequence'] = new $class; + } + return $this->modules['sequence']; + } + + /** + * Gets the default (preferred) Id generation strategy of the database platform. + * + * @todo Sure, the id generator types are more ORM functionality but they're + * still kind of dbal related. Maybe we need another set of classes (DatabasePlatform?) + * but im not so sure... + */ + /*abstract*/ public function getDefaultIdGeneratorType() + { + + } } diff --git a/lib/Doctrine/Connection/UnitOfWork.php b/lib/Doctrine/Connection/UnitOfWork.php index 864cd918f..62938ba59 100644 --- a/lib/Doctrine/Connection/UnitOfWork.php +++ b/lib/Doctrine/Connection/UnitOfWork.php @@ -21,42 +21,26 @@ #namespace Doctrine::ORM::Internal; +#use Doctrine::ORM::Entity; +#use Doctrine::ORM::EntityManager; +#use Doctrine::ORM::Exceptions::UnitOfWorkException; + /** * The UnitOfWork is responsible for tracking changes to objects during an - * "object-level" transaction and for writing out changes to the database at + * "object-level" transaction and for writing out changes to the database * in the correct order. - * - * Some terminology: - * - * New entity: A new entity is an entity that already has an identity but - * is not yet persisted into the database. This is usually the case for all - * newly saved/persisted entities that use a SEQUENCE id generator. Entities with an - * IDENTITY id generator get persisted as soon as they're saved in order to - * obtain the identifier. Therefore entities that use an IDENTITY id generator - * never appear in the list of new entities of the UoW. - * New entities are inserted into the database when the is UnitOfWork committed. - * - * Dirty entity: A dirty entity is a managed entity whose values have - * been altered. - * - * Removed entity: A removed entity is a managed entity that is scheduled - * for deletion from the database. - * - * Clean entity: A clean entity is a managed entity that has been fetched - * from the database and whose values have not yet been altered. * - * @package Doctrine - * @subpackage Connection * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.phpdoctrine.org * @since 2.0 * @version $Revision$ * @author Konsta Vesterinen * @author Roman Borschel - * @todo package:orm. Figure out a useful implementation. + * @todo Rename: Doctrine::ORM::(Internal::)UnitOfWork. + * @todo Turn connection exceptions into UnitOfWorkExceptions. */ class Doctrine_Connection_UnitOfWork -{ +{ /** * The identity map that holds references to all managed entities that have * an identity. The entities are grouped by their class name. @@ -66,36 +50,50 @@ class Doctrine_Connection_UnitOfWork * @var array */ protected $_identityMap = array(); - + /** - * A list of all new entities. + * A list of all new entities that need to be INSERTed. + * + * @var array + * @todo Index by class name. + * @todo Rename to _inserts? */ protected $_newEntities = array(); - + /** * A list of all dirty entities. + * + * @var array + * @todo Rename to _updates? */ protected $_dirtyEntities = array(); - + /** - * A list of all removed entities. + * A list of all deleted entities. + * Removed entities are entities that are "scheduled for removal" but have + * not yet been removed from the database. + * + * @var array + * @todo Rename to _deletions? */ - protected $_removedEntities = array(); - + protected $_deletedEntities = array(); + /** * The EntityManager the UnitOfWork belongs to. + * + * @var Doctrine::ORM::EntityManager */ protected $_em; - + /** * The calculator used to calculate the order in which changes to * entities need to be written to the database. * - * @var unknown_type + * @var Doctrine::ORM::Internal::CommitOrderCalculator * @todo Implementation. Replace buildFlushTree(). */ protected $_commitOrderCalculator; - + /** * Constructor. * Creates a new UnitOfWork. @@ -105,344 +103,375 @@ class Doctrine_Connection_UnitOfWork public function __construct(Doctrine_EntityManager $em) { $this->_em = $em; + //TODO: any benefit with lazy init? + $this->_commitOrderCalculator = new Doctrine_Internal_CommitOrderCalculator(); } - + /** * Commits the unit of work, executing all operations that have been postponed * up to this point. - * + * * @todo Impl */ public function commit() { - $this->_orderCommits(); - - $this->_insertNew(); - $this->_updateDirty(); - $this->_deleteRemoved(); - } - - private function _orderCommits() - { + // Detect changes in managed entities (mark dirty) + //TODO: Consider using registerDirty() in Entity#set() instead if its + // more performant. + foreach ($this->_identityMap as $entities) { + foreach ($entities as $entity) { + if ($entity->_state() == Doctrine_Entity::STATE_MANAGED + && $entity->isModified()) { + $this->registerDirty($entity); + } + } + } + if (empty($this->_newEntities) && + empty($this->_deletedEntities) && + empty($this->_dirtyEntities)) { + return; // Nothing to do. + } + + // Now we need a commit order to maintain referential integrity + $commitOrder = $this->_getCommitOrder(); + + //TODO: begin transaction here? + + foreach ($commitOrder as $class) { + $this->_executeInserts($class); + $this->_executeUpdates($class); + } + + // Deletions come last and need to be in reverse commit order + for ($count = count($commitOrder), $i = $count - 1; $i >= 0; $i--) { + $this->_executeDeletions($commitOrder[$i]); + } + + //TODO: commit transaction here? + + // clear lists + $this->_newEntities = array(); + $this->_dirtyEntities = array(); + $this->_deletedEntities = array(); } - + + private function _executeInserts($class) + { + //TODO: Maybe $persister->addInsert($entity) in the loop and + // $persister->executeInserts() at the end to allow easy prepared + // statement reuse and maybe bulk operations in the persister. + // Same for update/delete. + $className = $class->getClassName(); + $persister = $this->_em->getEntityPersister($className); + foreach ($this->_newEntities as $entity) { + if ($entity->getClass()->getClassName() == $className) { + $persister->insert($entity); + } + } + } + + private function _executeUpdates($class) + { + $className = $class->getClassName(); + $persister = $this->_em->getEntityPersister($className); + foreach ($this->_dirtyEntities as $entity) { + if ($entity->getClass()->getClassName() == $className) { + $persister->update($entity); + } + } + } + + private function _executeDeletions($class) + { + $className = $class->getClassName(); + $persister = $this->_em->getEntityPersister($className); + foreach ($this->_deletedEntities as $entity) { + if ($entity->getClass()->getClassName() == $className) { + $persister->delete($entity); + } + } + } + + /** + * Gets the commit order. + * + * @return array + */ + private function _getCommitOrder() + { + //TODO: Once these 3 arrays are indexed by classname we can do this: + // Either way... do we need to care about duplicates? + /*$classesInChangeSet = array_merge( + array_keys($this->_newEntities), + array_keys($this->_dirtyEntities), + array_keys($this->_deletedEntities) + );*/ + + $entityChangeSet = array_merge($this->_newEntities, $this->_dirtyEntities, $this->_deletedEntities); + + /* if (count($entityChangeSet) == 1) { + * return array($entityChangeSet[0]->getClass()); + * } + */ + + // See if there are any new classes in the changeset, that are not in the + // commit order graph yet (dont have a node). + $newNodes = array(); + foreach ($entityChangeSet as $entity) { + if ( ! $this->_commitOrderCalculator->hasNodeWithKey($entity->getClass()->getClassName())) { + $this->_commitOrderCalculator->addNodeWithItem( + $entity->getClass()->getClassName(), // index/key + $entity->getClass() // item + ); + $newNodes[] = $this->_commitOrderCalculator->getNodeForKey($entity->getClass()->getClassName()); + } + } + + // Calculate dependencies for new nodes + foreach ($newNodes as $node) { + foreach ($node->getClass()->getAssociationMappings() as $assocMapping) { + //TODO: should skip target classes that are not in the changeset. + if ($assocMapping->isOwningSide()) { + $targetClass = $assocMapping->getTargetClass(); + $targetClassName = $targetClass->getClassName(); + // if the target class does not yet have a node, create it + if ( ! $this->_commitOrderCalculator->hasNodeWithKey($targetClassName)) { + $this->_commitOrderCalculator->addNodeWithItem( + $targetClassName, // index/key + $targetClass // item + ); + } + // add dependency + $otherNode = $this->_commitOrderCalculator->getNodeForKey($targetClassName); + $node->before($otherNode); + } + } + } + + return $this->_commitOrderCalculator->getCommitOrder(); + } + /** * Register a new entity. + * + * @todo Rename to scheduleForInsert(). */ public function registerNew(Doctrine_Entity $entity) { - if ( ! $entity->identifier()) { - throw new Doctrine_Connection_Exception("Entity without identity " - . "can't be registered as new."); - } - $oid = $entity->getOid(); - + + /*if ( ! $entity->_identifier()) { + throw new Doctrine_Connection_Exception("Entity without identity cant be registered as new."); + }*/ if (isset($this->_dirtyEntities[$oid])) { throw new Doctrine_Connection_Exception("Dirty object can't be registered as new."); - } else if (isset($this->_removedEntities[$oid])) { + } + if (isset($this->_deletedEntities[$oid])) { throw new Doctrine_Connection_Exception("Removed object can't be registered as new."); - } else if (isset($this->_newEntities[$oid])) { + } + if (isset($this->_newEntities[$oid])) { throw new Doctrine_Connection_Exception("Object already registered as new. Can't register twice."); } - - $this->registerIdentity($entity); + $this->_newEntities[$oid] = $entity; + if ($entity->_identifier()) { + $this->addToIdentityMap($entity); + } } - + + /** + * Checks whether an entity is registered as new on the unit of work. + * + * @param Doctrine_Entity $entity + * @return boolean + * @todo Rename to isScheduledForInsert(). + */ public function isRegisteredNew(Doctrine_Entity $entity) { return isset($this->_newEntities[$entity->getOid()]); } - + /** * Registers a clean entity. + * The entity is simply put into the identity map. + * + * @param Doctrine::ORM::Entity $entity */ public function registerClean(Doctrine_Entity $entity) { - $this->registerIdentity($entity); + $this->addToIdentityMap($entity); } - + /** * Registers a dirty entity. + * + * @param Doctrine::ORM::Entity $entity + * @todo Rename to scheduleForUpdate(). */ public function registerDirty(Doctrine_Entity $entity) { - if ( ! $entity->identifier()) { + $oid = $entity->getOid(); + if ( ! $entity->_identifier()) { throw new Doctrine_Connection_Exception("Entity without identity " . "can't be registered as dirty."); } - $oid = $entity->getOid(); - if (isset($this->_removedEntities[$entity->getOid()])) { + if (isset($this->_deletedEntities[$oid])) { throw new Doctrine_Connection_Exception("Removed object can't be registered as dirty."); } - if ( ! isset($this->_dirtyEntities[$oid], $this->_newEntities[$oid])) { - $this->_dirtyEntities[$entity->getOid()] = $entity; + + if ( ! isset($this->_dirtyEntities[$oid]) && ! isset($this->_newEntities[$oid])) { + $this->_dirtyEntities[$oid] = $entity; } } - + + /** + * Checks whether an entity is registered as dirty in the unit of work. + * Note: Is not very useful currently as dirty entities are only registered + * at commit time. + * + * @param Doctrine_Entity $entity + * @return boolean + * @todo Rename to isScheduledForUpdate(). + */ public function isRegisteredDirty(Doctrine_Entity $entity) { return isset($this->_dirtyEntities[$entity->getOid()]); } - - /** + + /** * Registers a deleted entity. + * + * @todo Rename to scheduleForDelete(). */ - public function registerRemoved(Doctrine_Entity $entity) + public function registerDeleted(Doctrine_Entity $entity) { - if ($entity->isNew()) { + $oid = $entity->getOid(); + if ( ! $this->isInIdentityMap($entity)) { return; } - $this->unregisterIdentity($entity); - $oid = $entity->getOid(); + + $this->removeFromIdentityMap($entity); + if (isset($this->_newEntities[$oid])) { unset($this->_newEntities[$oid]); - return; + return; // entity has not been persisted yet, so nothing more to do. } - if (isset($this->_dirtyEntities[$oid])) { - unset($this->_dirtyEntities[$oid]); - } - if ( ! isset($this->_removedEntities[$oid])) { - $this->_removedEntities[$oid] = $entity; + /* Seems unnecessary since _dirtyEntities is filled & cleared on commit, not earlier + if (isset($this->_dirtyEntities[$oid])) { + unset($this->_dirtyEntities[$oid]); + }*/ + if ( ! isset($this->_deletedEntities[$oid])) { + $this->_deletedEntities[$oid] = $entity; } } - + + /** + * Checks whether an entity is registered as removed/deleted with the unit + * of work. + * + * @param Doctrine::ORM::Entity $entity + * @return boolean + * @todo Rename to isScheduledForDelete(). + */ public function isRegisteredRemoved(Doctrine_Entity $entity) { - return isset($this->_removedEntities[$entity->getOid()]); + return isset($this->_deletedEntities[$entity->getOid()]); } /** - * builds a flush tree that is used in transactions + * Detaches an entity from the persistence management. It's persistence will + * no longer be managed by Doctrine. * - * The returned array has all the initialized components in - * 'correct' order. Basically this means that the records of those - * components can be saved safely in the order specified by the returned array. - * - * @param array $tables an array of Doctrine_Table objects or component names - * @return array an array of component names in flushing order - */ - public function buildFlushTree(array $entityNames) - { - $tree = array(); - foreach ($entityNames as $k => $entity) { - if ( ! ($mapper instanceof Doctrine_Mapper)) { - $mapper = $this->conn->getMapper($mapper); - } - $nm = $mapper->getComponentName(); - - $index = array_search($nm, $tree); - - if ($index === false) { - $tree[] = $nm; - $index = max(array_keys($tree)); - } - - $rels = $mapper->getClassMetadata()->getRelations(); - - // group relations - - foreach ($rels as $key => $rel) { - if ($rel instanceof Doctrine_Relation_ForeignKey) { - unset($rels[$key]); - array_unshift($rels, $rel); - } - } - - foreach ($rels as $rel) { - $name = $rel->getTable()->getComponentName(); - $index2 = array_search($name, $tree); - $type = $rel->getType(); - - // skip self-referenced relations - if ($name === $nm) { - continue; - } - - if ($rel instanceof Doctrine_Relation_ForeignKey) { - if ($index2 !== false) { - if ($index2 >= $index) - continue; - - unset($tree[$index]); - array_splice($tree,$index2,0,$nm); - $index = $index2; - } else { - $tree[] = $name; - } - } else if ($rel instanceof Doctrine_Relation_LocalKey) { - if ($index2 !== false) { - if ($index2 <= $index) - continue; - - unset($tree[$index2]); - array_splice($tree, $index, 0, $name); - } else { - array_unshift($tree,$name); - $index++; - } - } else if ($rel instanceof Doctrine_Relation_Association) { - $t = $rel->getAssociationFactory(); - $n = $t->getComponentName(); - - if ($index2 !== false) { - unset($tree[$index2]); - } - - array_splice($tree, $index, 0, $name); - $index++; - - $index3 = array_search($n, $tree); - - if ($index3 !== false) { - if ($index3 >= $index) - continue; - - unset($tree[$index]); - array_splice($tree, $index3, 0, $n); - $index = $index2; - } else { - $tree[] = $n; - } - } - } - } - - return $tree; - } - - /** - * persists all the pending records from all tables - * - * @throws PDOException if something went wrong at database level - * @return void - * @deprecated - */ - /*public function saveAll() - { - $this->conn->beginInternalTransaction(); - // get the flush tree - $tree = $this->buildFlushTree($this->conn->getMappers()); - - $tree = array_combine($tree, array_fill(0, count($tree), array())); - - foreach ($this->_managedEntities as $oid => $entity) { - $className = $entity->getClassName(); - $tree[$className][] = $entity; - } - - // save all records - foreach ($tree as $className => $entities) { - $mapper = $this->conn->getMapper($className); - foreach ($entities as $entity) { - $mapper->saveSingleRecord($entity); - } - } - - // save all associations - foreach ($tree as $className => $entities) { - $mapper = $this->conn->getMapper($className); - foreach ($entities as $entity) { - $mapper->saveAssociations($entity); - } - } - $this->conn->commit(); - }*/ - - /** - * Adds an entity to the pool of managed entities. - * @deprecated - */ - public function manage(Doctrine_Entity $entity) - { - $oid = $entity->getOid(); - if ( ! isset($this->_managedEntities[$oid])) { - $this->_managedEntities[$oid] = $entity; - return true; - } - return false; - } - - /** * @param integer $oid object identifier * @return boolean whether ot not the operation was successful - * @deprecated The new implementation of detach() should remove the entity - * from the identity map. */ public function detach(Doctrine_Entity $entity) { - $oid = $entity->getOid(); - if ( ! isset($this->_managedEntities[$oid])) { - return false; + if ($this->isInIdentityMap($entity)) { + $this->removeFromIdentityMap($entity); } - unset($this->_managedEntities[$oid]); - return true; } - + + /** + * Enter description here... + * + * @param Doctrine_Entity $entity + * @return unknown + * @todo Rename to isScheduled() + */ + public function isEntityRegistered(Doctrine_Entity $entity) + { + $oid = $entity->getOid(); + return isset($this->_newEntities[$oid]) || + //isset($this->_dirtyEntities[$oid]) || + isset($this->_deletedEntities[$oid]) || + $this->isInIdentityMap($entity); + } + /** * Detaches all currently managed entities. + * Alternatively, if an entity class name is given, all entities of that type + * (or subtypes) are detached. Don't forget that entities are registered in + * the identity map with the name of the root entity class. So calling detachAll() + * with a class name that is not the name of a root entity has no effect. * * @return integer The number of detached entities. - * @todo Deprecated. The new implementation should remove all entities from - * the identity map. */ - public function detachAll() + public function detachAll($entityName = null) { - $numDetached = count($this->_managedEntities); - $this->_managedEntities = array(); + //TODO: what do do with new/dirty/removed lists? + $numDetached = 0; + if ($entityName !== null && isset($this->_identityMap[$entityName])) { + $numDetached = count($this->_identityMap[$entityName]); + $this->_identityMap[$entityName] = array(); + } else { + $numDetached = count($this->_identityMap); + $this->_identityMap = array(); + } + return $numDetached; } - + /** * Registers an entity in the identity map. - * + * Note that entities in a hierarchy are registered with the class name of + * the root entity. + * + * @param Doctrine::ORM::Entity $entity The entity to register. * @return boolean TRUE if the registration was successful, FALSE if the identity of * the entity in question is already managed. - * @throws Doctrine_Connection_Exception If the entity has no (database) identity. */ - public function registerIdentity(Doctrine_Entity $entity) + public function addToIdentityMap(Doctrine_Entity $entity) { - $idHash = $this->getIdentifierHash($entity->identifier()); - if ( ! $idHash) { + $idHash = $this->getIdentifierHash($entity->_identifier()); + if ($idHash === '') { throw new Doctrine_Connection_Exception("Entity with oid '" . $entity->getOid() . "' has no identity and therefore can't be added to the identity map."); } - $className = $entity->getClassMetadata()->getRootClassName(); + $className = $entity->getClass()->getRootClassName(); if (isset($this->_identityMap[$className][$idHash])) { return false; } $this->_identityMap[$className][$idHash] = $entity; + $entity->_state(Doctrine_Entity::STATE_MANAGED); return true; } - - /** - * Enter description here... - * - * @param unknown_type $entityName - * @todo unify with detachAll() - */ - public function clearIdentitiesForEntity($entityName) - { - $this->_identityMap[$entityName] = array(); - } - + /** * Removes an entity from the identity map. * * @param Doctrine_Entity $entity * @return unknown - * @todo This will be the new detach(). */ - public function unregisterIdentity(Doctrine_Entity $entity) + public function removeFromIdentityMap(Doctrine_Entity $entity) { - $idHash = $this->getIdentifierHash($entity->identifier()); - if ( ! $idHash) { + $idHash = $this->getIdentifierHash($entity->_identifier()); + if ($idHash === '') { throw new Doctrine_Connection_Exception("Entity with oid '" . $entity->getOid() . "' has no identity and therefore can't be removed from the identity map."); } - $className = $entity->getClassMetadata()->getRootClassName(); + $className = $entity->getClass()->getRootClassName(); if (isset($this->_identityMap[$className][$idHash])) { unset($this->_identityMap[$className][$idHash]); return true; @@ -450,19 +479,18 @@ class Doctrine_Connection_UnitOfWork return false; } - + /** * Finds an entity in the identity map by its identifier hash. * - * @param unknown_type $idHash - * @param unknown_type $rootClassName - * @return unknown + * @param string $idHash + * @param string $rootClassName + * @return Doctrine::ORM::Entity */ public function getByIdHash($idHash, $rootClassName) { return $this->_identityMap[$rootClassName][$idHash]; } - public function tryGetByIdHash($idHash, $rootClassName) { if ($this->containsIdHash($idHash, $rootClassName)) { @@ -470,36 +498,43 @@ class Doctrine_Connection_UnitOfWork } return false; } - + /** * Gets the identifier hash for a set of identifier values. + * The hash is just a concatenation of the identifier values. + * The identifiers are concatenated with a space. + * + * Note that this method always returns a string. If the given array is + * empty, an empty string is returned. * * @param array $id - * @return string + * @return string The hash. */ public function getIdentifierHash(array $id) { return implode(' ', $id); } - + /** - * Checks whether an entity is registered in the identity map. + * Checks whether an entity is registered in the identity map of the + * UnitOfWork. * * @param Doctrine_Entity $entity * @return boolean */ - public function contains(Doctrine_Entity $entity) + public function isInIdentityMap(Doctrine_Entity $entity) { - $idHash = $this->getIdentifierHash($entity->identifier()); - if ( ! $idHash) { + $idHash = $this->getIdentifierHash($entity->_identifier()); + if ($idHash === '') { return false; } - + return isset($this->_identityMap - [$entity->getClassMetadata()->getRootClassName()] - [$idHash]); + [$entity->getClass()->getRootClassName()] + [$idHash] + ); } - + /** * Checks whether an identifier hash exists in the identity map. * @@ -511,46 +546,167 @@ class Doctrine_Connection_UnitOfWork { return isset($this->_identityMap[$rootClassName][$idHash]); } - + + /** + * Saves an entity as part of the current unit of work. + * + * @param Doctrine_Entity $entity The entity to save. + */ public function save(Doctrine_Entity $entity) - { - switch ($entity->_state()) { - case Doctrine_Entity::STATE_CLEAN: - //nothing to do - // ignore $entity but cascade - break; - case Doctrine_Entity::STATE_DIRTY: - // update - $this->registerDirty($entity); - // todo:cascade - break; - case Doctrine_Entity::STATE_TCLEAN: - case Doctrine_Entity::STATE_TDIRTY: - // insert - // if identifier type IDENTITY: - // cascade - // if no transaction is started yet, do it - // force insert (directly to persister) - // else - // cascade - // get & assign the identifier, then registerNew() - break; + { + $insertNow = array(); + $visited = array(); + $this->_doSave($entity, $visited, $insertNow); + if ( ! empty($insertNow)) { + // We have no choice. This means that there are either new entities + // with an IDENTITY key generation or with a natural identifier. + // In both cases we must commit the inserts instantly. + //TODO: Isnt it enough to only execute the inserts instead of full flush? + $this->commit(); } } - - private function _cascadeSave(Doctrine_Entity $entity) + + /** + * Saves an entity as part of the current unit of work. + * This method is internally called during save() cascades as it tracks + * the already visited entities to prevent infinite recursions. + * + * @param Doctrine_Entity $entity The entity to save. + * @param array $visited The already visited entities. + */ + private function _doSave(Doctrine_Entity $entity, array &$visited, array &$insertNow) { + if (isset($visited[$entity->getOid()])) { + return; // Prevent infinite recursion + } + + $visited[$entity->getOid()] = $entity; // mark visited + + $class = $entity->getClass(); + switch ($entity->_state()) { + case Doctrine_Entity::STATE_MANAGED: + // nothing to do for $entity + break; + case Doctrine_Entity::STATE_NEW: + if ($class->isIdGeneratorIdentity()) { + $insertNow[$entity->getOid()] = $entity; + $this->_newEntities[$entity->getOid()] = $entity; + } else if ( ! $class->usesIdGenerator()) { + $insertNow[$entity->getOid()] = $entity; + //... + } else if ($class->isIdGeneratorSequence()) { + // Get the next sequence number + //TODO: sequence name? + $id = $this->_em->getConnection()->getSequenceModule()->nextId("foo"); + $entity->set($class->getSingleIdentifierFieldName(), $id); + $this->registerNew($entity); + } else { + throw new Doctrine_Exception("Unable to handle ID generation of new entity."); + } + break; + case Doctrine_Entity::STATE_DETACHED: + //exception? + throw new Doctrine_Exception("Behavior of save() for a detached entity " + . "is not yet defined."); + case Doctrine_Entity::STATE_DELETED: + // $entity becomes managed again + if ($this->isRegisteredRemoved($entity)) { + //TODO: better a method for this? + unset($this->_deletedEntities[$entity->getOid()]); + } else { + //FIXME: There's more to think of here... + $this->registerNew($entity); + } + break; + default: + //TODO: throw UnitOfWorkException::invalidEntityState() + throw new Doctrine_Exception("Encountered invalid entity state."); + } + $this->_cascadeSave($entity, $visited, $insertNow); } - + + /** + * Deletes an entity as part of the current unit of work. + * + * @param Doctrine_Entity $entity + */ + public function delete(Doctrine_Entity $entity) + { + $this->_doDelete($entity, array()); + } + + private function _doDelete(Doctrine_Entity $entity, array &$visited) + { + if (isset($visited[$entity->getOid()])) { + return; // Prevent infinite recursion + } + + $visited[$entity->getOid()] = $entity; // mark visited + + $class = $entity->getClass(); + switch ($entity->_state()) { + case Doctrine_Entity::STATE_NEW: + case Doctrine_Entity::STATE_DELETED: + // nothing to do for $entity + break; + case Doctrine_Entity::STATE_MANAGED: + $this->registerDeleted($entity); + break; + case Doctrine_Entity::STATE_DETACHED: + //exception? + throw new Doctrine_Exception("A detached entity can't be deleted."); + default: + //TODO: throw UnitOfWorkException::invalidEntityState() + throw new Doctrine_Exception("Encountered invalid entity state."); + } + + $this->_cascadeDelete($entity, $visited); + } + + /** + * Cascades the save operation to associated entities. + * + * @param Doctrine_Entity $entity + * @param array $visited + */ + private function _cascadeSave(Doctrine_Entity $entity, array &$visited, array &$insertNow) + { + foreach ($entity->getClass()->getAssociationMappings() as $assocMapping) { + if ( ! $assocMapping->isCascadeSave()) { + continue; + } + $relatedEntities = $entity->get($assocMapping->getSourceFieldName()); + if ($relatedEntities instanceof Doctrine_Entity) { + $this->_doSave($relatedEntities, $visited, $insertNow); + } else if ($relatedEntities instanceof Doctrine_Collection && + count($relatedEntities) > 0) { + foreach ($relatedEntities as $relatedEntity) { + $this->_doSave($relatedEntity, $visited, $insertNow); + } + } + } + } + private function _cascadeDelete(Doctrine_Entity $entity) { - + } + public function getCommitOrderCalculator() + { + return $this->_commitOrderCalculator; + } + + public function close() + { + //... + $this->_commitOrderCalculator->clear(); + } + // Stuff from 0.11/1.0 that we will need later (need to modify it though) - + /** * Collects all records that need to be deleted by applying defined * application-level delete cascades. @@ -558,15 +714,15 @@ class Doctrine_Connection_UnitOfWork * @param array $deletions Map of the records to delete. Keys=Oids Values=Records. */ /*private function _collectDeletions(Doctrine_Record $record, array &$deletions) - { - if ( ! $record->exists()) { - return; - } + { + if ( ! $record->exists()) { + return; + } + + $deletions[$record->getOid()] = $record; + $this->_cascadeDelete($record, $deletions); + }*/ - $deletions[$record->getOid()] = $record; - $this->_cascadeDelete($record, $deletions); - }*/ - /** * Cascades an ongoing delete operation to related objects. Applies only on relations * that have 'delete' in their cascade options. @@ -579,32 +735,32 @@ class Doctrine_Connection_UnitOfWork * @throws PDOException If something went wrong at database level * @return void */ - /*protected function _cascadeDelete(Doctrine_Record $record, array &$deletions) + /*protected function _cascadeDelete(Doctrine_Record $record, array &$deletions) { - foreach ($record->getTable()->getRelations() as $relation) { - if ($relation->isCascadeDelete()) { - $fieldName = $relation->getAlias(); - // if it's a xToOne relation and the related object is already loaded - // we don't need to refresh. - if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) { - $record->refreshRelated($relation->getAlias()); - } - $relatedObjects = $record->get($relation->getAlias()); - if ($relatedObjects instanceof Doctrine_Record && $relatedObjects->exists() - && ! isset($deletions[$relatedObjects->getOid()])) { - $this->_collectDeletions($relatedObjects, $deletions); - } else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) { - // cascade the delete to the other objects - foreach ($relatedObjects as $object) { - if ( ! isset($deletions[$object->getOid()])) { - $this->_collectDeletions($object, $deletions); - } - } - } - } - } + foreach ($record->getTable()->getRelations() as $relation) { + if ($relation->isCascadeDelete()) { + $fieldName = $relation->getAlias(); + // if it's a xToOne relation and the related object is already loaded + // we don't need to refresh. + if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) { + $record->refreshRelated($relation->getAlias()); + } + $relatedObjects = $record->get($relation->getAlias()); + if ($relatedObjects instanceof Doctrine_Record && $relatedObjects->exists() + && ! isset($deletions[$relatedObjects->getOid()])) { + $this->_collectDeletions($relatedObjects, $deletions); + } else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) { + // cascade the delete to the other objects + foreach ($relatedObjects as $object) { + if ( ! isset($deletions[$object->getOid()])) { + $this->_collectDeletions($object, $deletions); + } + } + } + } + } }*/ - + /** * Executes the deletions for all collected records during a delete operation * (usually triggered through $record->delete()). @@ -612,89 +768,89 @@ class Doctrine_Connection_UnitOfWork * @param array $deletions Map of the records to delete. Keys=Oids Values=Records. */ /*private function _executeDeletions(array $deletions) - { - // collect class names - $classNames = array(); - foreach ($deletions as $record) { - $classNames[] = $record->getTable()->getComponentName(); - } - $classNames = array_unique($classNames); + { + // collect class names + $classNames = array(); + foreach ($deletions as $record) { + $classNames[] = $record->getTable()->getComponentName(); + } + $classNames = array_unique($classNames); - // order deletes - $executionOrder = $this->buildFlushTree($classNames); + // order deletes + $executionOrder = $this->buildFlushTree($classNames); - // execute - try { - $this->conn->beginInternalTransaction(); + // execute + try { + $this->conn->beginInternalTransaction(); - for ($i = count($executionOrder) - 1; $i >= 0; $i--) { - $className = $executionOrder[$i]; - $table = $this->conn->getTable($className); + for ($i = count($executionOrder) - 1; $i >= 0; $i--) { + $className = $executionOrder[$i]; + $table = $this->conn->getTable($className); - // collect identifiers - $identifierMaps = array(); - $deletedRecords = array(); - foreach ($deletions as $oid => $record) { - if ($record->getTable()->getComponentName() == $className) { - $veto = $this->_preDelete($record); - if ( ! $veto) { - $identifierMaps[] = $record->identifier(); - $deletedRecords[] = $record; - unset($deletions[$oid]); - } - } - } + // collect identifiers + $identifierMaps = array(); + $deletedRecords = array(); + foreach ($deletions as $oid => $record) { + if ($record->getTable()->getComponentName() == $className) { + $veto = $this->_preDelete($record); + if ( ! $veto) { + $identifierMaps[] = $record->identifier(); + $deletedRecords[] = $record; + unset($deletions[$oid]); + } + } + } - if (count($deletedRecords) < 1) { - continue; - } + if (count($deletedRecords) < 1) { + continue; + } - // extract query parameters (only the identifier values are of interest) - $params = array(); - $columnNames = array(); - foreach ($identifierMaps as $idMap) { - while (list($fieldName, $value) = each($idMap)) { - $params[] = $value; - $columnNames[] = $table->getColumnName($fieldName); - } - } - $columnNames = array_unique($columnNames); + // extract query parameters (only the identifier values are of interest) + $params = array(); + $columnNames = array(); + foreach ($identifierMaps as $idMap) { + while (list($fieldName, $value) = each($idMap)) { + $params[] = $value; + $columnNames[] = $table->getColumnName($fieldName); + } + } + $columnNames = array_unique($columnNames); - // delete - $tableName = $table->getTableName(); - $sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE "; + // delete + $tableName = $table->getTableName(); + $sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE "; - if ($table->isIdentifierComposite()) { - $sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps)); - $this->conn->exec($sql, $params); - } else { - $sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params)); - $this->conn->exec($sql, $params); - } + if ($table->isIdentifierComposite()) { + $sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps)); + $this->conn->exec($sql, $params); + } else { + $sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params)); + $this->conn->exec($sql, $params); + } - // adjust state, remove from identity map and inform postDelete listeners - foreach ($deletedRecords as $record) { - // currently just for bc! - $this->_deleteCTIParents($table, $record); - //-- - $record->state(Doctrine_Record::STATE_TCLEAN); - $record->getTable()->removeRecord($record); - $this->_postDelete($record); - } - } + // adjust state, remove from identity map and inform postDelete listeners + foreach ($deletedRecords as $record) { + // currently just for bc! + $this->_deleteCTIParents($table, $record); + //-- + $record->state(Doctrine_Record::STATE_TCLEAN); + $record->getTable()->removeRecord($record); + $this->_postDelete($record); + } + } - $this->conn->commit(); - // trigger postDelete for records skipped during the deletion (veto!) - foreach ($deletions as $skippedRecord) { - $this->_postDelete($skippedRecord); - } + $this->conn->commit(); + // trigger postDelete for records skipped during the deletion (veto!) + foreach ($deletions as $skippedRecord) { + $this->_postDelete($skippedRecord); + } - return true; - } catch (Exception $e) { - $this->conn->rollback(); - throw $e; - } - }*/ + return true; + } catch (Exception $e) { + $this->conn->rollback(); + throw $e; + } + }*/ /** * Builds the SQL condition to target multiple records who have a single-column @@ -705,10 +861,10 @@ class Doctrine_Connection_UnitOfWork * @return string The SQL condition "pk = ? OR pk = ? OR pk = ? ..." */ /*private function _buildSqlSingleKeyCondition($columnNames, $numRecords) - { - $idColumn = $this->conn->quoteIdentifier($columnNames[0]); - return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?")); - }*/ + { + $idColumn = $this->conn->quoteIdentifier($columnNames[0]); + return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?")); + }*/ /** * Builds the SQL condition to target multiple records who have a composite primary key. @@ -718,21 +874,26 @@ class Doctrine_Connection_UnitOfWork * @return string The SQL condition "(pk1 = ? AND pk2 = ?) OR (pk1 = ? AND pk2 = ?) ..." */ /*private function _buildSqlCompositeKeyCondition($columnNames, $numRecords) - { - $singleCondition = ""; - foreach ($columnNames as $columnName) { - $columnName = $this->conn->quoteIdentifier($columnName); - if ($singleCondition === "") { - $singleCondition .= "($columnName = ?"; - } else { - $singleCondition .= " AND $columnName = ?"; - } - } - $singleCondition .= ")"; - $fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition)); + { + $singleCondition = ""; + foreach ($columnNames as $columnName) { + $columnName = $this->conn->quoteIdentifier($columnName); + if ($singleCondition === "") { + $singleCondition .= "($columnName = ?"; + } else { + $singleCondition .= " AND $columnName = ?"; + } + } + $singleCondition .= ")"; + $fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition)); - return $fullCondition; - }*/ + return $fullCondition; + }*/ + + public function getIdentityMap() + { + return $this->_identityMap; + } } diff --git a/lib/Doctrine/Entity.php b/lib/Doctrine/Entity.php index 86d50763b..5e7bac542 100644 --- a/lib/Doctrine/Entity.php +++ b/lib/Doctrine/Entity.php @@ -37,45 +37,23 @@ * @link www.phpdoctrine.org * @since 2.0 * @version $Revision: 4342 $ - * @todo Split up into "Entity" and "ActiveEntity" (extends Entity) - * @todo Move entity states into a separate enumeration (EntityStates). - * They do not need to be exposed to users in such a way. The states are mainly - * for internal use. + * @todo Split up into "Entity" and "ActiveEntity" (extends Entity). */ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable { /** - * DIRTY STATE - * An Entity is in dirty state when its properties are changed. + * MANAGED + * An Entity is in managed state when it has a primary key/identifier and is + * managed by an EntityManager (registered in the identity map). */ - const STATE_DIRTY = 1; - const STATE_MANAGED_DIRTY = 1; + const STATE_MANAGED = 1; /** - * TDIRTY STATE - * An Entity is in transient dirty state when it is created and some of its - * fields are modified but it is NOT yet persisted into database. + * NEW + * An Entity is new if it does not yet have an identifier/primary key + * and is not (yet) managed by an EntityManager. */ - const STATE_TDIRTY = 2; - const STATE_NEW_DIRTY = 2; - - /** - * CLEAN STATE - * An Entity is in clean state when all of its properties are loaded from the database - * and none of its properties are changed. - */ - const STATE_CLEAN = 3; - const STATE_MANAGED_CLEAN = 3; - - /** - * NEW TCLEAN - * An Entity is in transient clean state when it is created and none of its - * fields are modified. - * @todo Do we need this state? Just STATE_NEW may be enough without differentiating - * clean/dirty. A new entity is always "dirty". - */ - const STATE_TCLEAN = 5; - const STATE_NEW_CLEAN = 5; + const STATE_NEW = 2; /** * LOCKED STATE @@ -93,14 +71,14 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable * (or no longer) associated with an EntityManager (and a UnitOfWork). * This means its no longer in the identity map. */ - const STATE_DETACHED = 7; + const STATE_DETACHED = 3; /** * A removed Entity instance is an instance with a persistent identity, * associated with an EntityManager, that is scheduled for removal from the * database. */ - const STATE_DELETED = 8; + const STATE_DELETED = 4; /** * Index used for creating object identifiers (oid's). @@ -144,12 +122,6 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable */ private $_entityName; - /** - * @var Doctrine_Node_ node object - * @todo Specific to the NestedSet Behavior plugin. Move outta here. - */ - //protected $_node; - /** * The values that make up the ID/primary key of the entity. * @@ -167,16 +139,16 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable /** * The state of the object. * - * @var integer - * @see STATE_* constants + * @var integer */ private $_state; /** * The names of fields that have been modified but not yet persisted. + * Keys are field names, values oldValue => newValue tuples. * - * @var array - * @todo Better name? $_modifiedFields? + * @var array + * @todo Rename to $_changeSet */ private $_modified = array(); @@ -204,6 +176,7 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable /** * Constructor. + * Creates a new Entity instance. */ public function __construct() { @@ -214,9 +187,9 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable $this->_data = $this->_em->_getTmpEntityData(); if ($this->_data) { $this->_extractIdentifier(); - $this->_state = self::STATE_CLEAN; + $this->_state = self::STATE_MANAGED; } else { - $this->_state = self::STATE_TCLEAN; + $this->_state = self::STATE_NEW; } // @todo read from attribute the first time and move this initialization elsewhere. @@ -265,7 +238,7 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable }*/ /** - * hydrates this object from given array + * Hydrates this object from given array * * @param array $data * @return boolean @@ -278,33 +251,26 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable /** * Copies the identifier names and values from _data into _id. - * - * @param boolean $exists whether or not this record exists in persistent data store - * @return void - * @todo Looks like its better placed elsewhere (EntityManager?) */ private function _extractIdentifier() { - switch ($this->_class->getIdentifierType()) { - case Doctrine::IDENTIFIER_AUTOINC: - case Doctrine::IDENTIFIER_SEQUENCE: - case Doctrine::IDENTIFIER_NATURAL: - $name = $this->_class->getIdentifier(); - $name = $name[0]; - if (isset($this->_data[$name]) && $this->_data[$name] !== Doctrine_Null::$INSTANCE) { + if ( ! $this->_class->isIdentifierComposite()) { + // Single field identifier + $name = $this->_class->getIdentifier(); + $name = $name[0]; + if (isset($this->_data[$name]) && $this->_data[$name] !== Doctrine_Null::$INSTANCE) { + $this->_id[$name] = $this->_data[$name]; + } + } else { + // Composite identifier + $names = $this->_class->getIdentifier(); + foreach ($names as $name) { + if ($this->_data[$name] === Doctrine_Null::$INSTANCE) { + $this->_id[$name] = null; + } else { $this->_id[$name] = $this->_data[$name]; } - break; - case Doctrine::IDENTIFIER_COMPOSITE: - $names = $this->_class->getIdentifier(); - foreach ($names as $name) { - if ($this->_data[$name] === Doctrine_Null::$INSTANCE) { - $this->_id[$name] = null; - } else { - $this->_id[$name] = $this->_data[$name]; - } - } - break; + } } } @@ -438,22 +404,16 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable /* TODO: Do we really need this check? This is only for internal use after all. */ switch ($state) { - case self::STATE_TCLEAN: - case self::STATE_CLEAN: - case self::STATE_TDIRTY: - case self::STATE_DIRTY: - case self::STATE_PROXY: + case self::STATE_MANAGED: + case self::STATE_DELETED: + case self::STATE_DETACHED: + case self::STATE_NEW: case self::STATE_LOCKED: $this->_state = $state; break; default: throw Doctrine_Entity_Exception::invalidState($state); } - - if ($this->_state === Doctrine_Entity::STATE_TCLEAN || - $this->_state === Doctrine_Entity::STATE_CLEAN) { - $this->_modified = array(); - } } /** @@ -878,23 +838,13 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable }*/ $old = isset($this->_data[$fieldName]) ? $this->_data[$fieldName] : null; - + //FIXME: null == 0 => true if ($old != $value) { $this->_data[$fieldName] = $value; $this->_modified[$fieldName] = array($old => $value); - if ($this->isNew() && $this->_class->isIdentifier($fieldName)) { $this->_id[$fieldName] = $value; } - - switch ($this->_state) { - case Doctrine_Entity::STATE_CLEAN: - $this->_state = Doctrine_Entity::STATE_DIRTY; - break; - case Doctrine_Entity::STATE_TCLEAN: - $this->_state = Doctrine_Entity::STATE_TDIRTY; - break; - } } } else if ($this->_class->hasRelation($fieldName)) { $this->_rawSetReference($fieldName, $value); @@ -1253,7 +1203,7 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable */ final public function isNew() { - return $this->_state == self::STATE_TCLEAN || $this->_state == self::STATE_TDIRTY; + return $this->_state == self::STATE_NEW; } /** @@ -1264,8 +1214,7 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable */ final public function isModified() { - return ($this->_state === Doctrine_Entity::STATE_DIRTY || - $this->_state === Doctrine_Entity::STATE_TDIRTY); + return count($this->_modified) > 0; } /** @@ -1349,44 +1298,33 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable /** * INTERNAL: + * Assigns an identifier to the entity. This is only intended for use by + * the EntityPersisters or the UnitOfWork. * - * @param integer $id - * @return void - * @todo Not sure this is the right place here. + * @param mixed $id */ - final public function assignIdentifier($id = false) + final public function _assignIdentifier($id) { - if ($id === false) { - $this->_id = array(); - $this->_state = Doctrine_Entity::STATE_TCLEAN; - $this->_modified = array(); - } else if ($id === true) { - $this->_extractIdentifier(true); - $this->_state = Doctrine_Entity::STATE_CLEAN; - $this->_modified = array(); - } else { - if (is_array($id)) { - foreach ($id as $fieldName => $value) { - $this->_id[$fieldName] = $value; - $this->_data[$fieldName] = $value; - } - } else { - $idFieldNames = $this->_class->getIdentifier(); - $name = $idFieldNames[0]; - $this->_id[$name] = $id; - $this->_data[$name] = $id; + if (is_array($id)) { + foreach ($id as $fieldName => $value) { + $this->_id[$fieldName] = $value; + $this->_data[$fieldName] = $value; } - $this->_state = self::STATE_CLEAN; - $this->_modified = array(); + } else { + $name = $this->_class->getSingleIdentifierFieldName(); + $this->_id[$name] = $id; + $this->_data[$name] = $id; } + $this->_modified = array(); } /** - * returns the primary keys of this object + * INTERNAL: + * Returns the primary keys of the entity (key => value pairs). * * @return array */ - final public function identifier() + final public function _identifier() { return $this->_id; } @@ -1656,13 +1594,13 @@ abstract class Doctrine_Entity extends Doctrine_Access implements Serializable { $this->getNode()->delete(); }*/ - + /** * Gets the ClassMetadata object that describes the entity class. * * @return Doctrine::ORM::Mapping::ClassMetadata */ - final public function getClassMetadata() + final public function getClass() { return $this->_class; } diff --git a/lib/Doctrine/EntityManager.php b/lib/Doctrine/EntityManager.php index 973d2cd92..1bb19182d 100644 --- a/lib/Doctrine/EntityManager.php +++ b/lib/Doctrine/EntityManager.php @@ -42,6 +42,10 @@ */ class Doctrine_EntityManager { + const FLUSHMODE_AUTO = 'auto'; + const FLUSHMODE_COMMIT = 'commit'; + const FLUSHMODE_MANUAL = 'manual'; + /** * The unique name of the EntityManager. The name is used to bind entity classes * to certain EntityManagers. @@ -70,11 +74,11 @@ class Doctrine_EntityManager private static $_flushModes = array( // auto: Flush occurs automatically after each operation that issues database // queries. No operations are queued. - 'auto', + self::FLUSHMODE_AUTO, // commit: Flush occurs automatically at transaction commit. - 'commit', + self::FLUSHMODE_COMMIT, // manual: Flush occurs never automatically. - 'manual' + self::FLUSHMODE_MANUAL ); /** @@ -242,7 +246,7 @@ class Doctrine_EntityManager */ public function detach(Doctrine_Entity $entity) { - return $this->_unitOfWork->unregisterIdentity($entity); + return $this->_unitOfWork->removeFromIdentityMap($entity); } /** @@ -287,7 +291,7 @@ class Doctrine_EntityManager */ public function flush() { - $this->_unitOfWork->flush(); + $this->_unitOfWork->commit(); } /** @@ -386,6 +390,9 @@ class Doctrine_EntityManager public function save(Doctrine_Entity $entity) { $this->_unitOfWork->save($entity); + if ($this->_flushMode == self::FLUSHMODE_AUTO) { + $this->flush(); + } } /** @@ -453,7 +460,7 @@ class Doctrine_EntityManager return $entity; } else { $entity = new $className; - $this->_unitOfWork->registerIdentity($entity); + $this->_unitOfWork->addToIdentityMap($entity); } } } else { @@ -469,6 +476,17 @@ class Doctrine_EntityManager return $entity; } + /** + * Checks if the instance is managed by the EntityManager. + * + * @return boolean + */ + public function contains(Doctrine_Entity $entity) + { + return $this->_unitOfWork->isInIdentityMap($entity) && + ! $this->_unitOfWork->isRegisteredRemoved($entity); + } + /** * INTERNAL: * For internal hydration purposes only. @@ -546,7 +564,7 @@ class Doctrine_EntityManager } /** - * Gets the COnfiguration used by the EntityManager. + * Gets the Configuration used by the EntityManager. * * @return Configuration */ @@ -555,6 +573,16 @@ class Doctrine_EntityManager return $this->_config; } + /** + * Gets the UnitOfWork used by the EntityManager to coordinate operations. + * + * @return Doctrine::ORM::UnitOfWork + */ + public function getUnitOfWork() + { + return $this->_unitOfWork; + } + } ?> \ No newline at end of file diff --git a/lib/Doctrine/EventListener.php b/lib/Doctrine/EventSubscriber.php similarity index 76% rename from lib/Doctrine/EventListener.php rename to lib/Doctrine/EventSubscriber.php index 6a4c4c9bc..b14cb0488 100644 --- a/lib/Doctrine/EventListener.php +++ b/lib/Doctrine/EventSubscriber.php @@ -1,6 +1,6 @@ - * @package Doctrine - * @subpackage EventListener * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.phpdoctrine.org * @since 2.0 - * @version $Revision$ - * @todo Remove. The 2.0 event system has no listener interfaces. + * @version $Revision: 4653 $ */ interface Doctrine_EventSubscriber { diff --git a/lib/Doctrine/Exception.php b/lib/Doctrine/Exception.php index 7e52387d3..e175073b0 100644 --- a/lib/Doctrine/Exception.php +++ b/lib/Doctrine/Exception.php @@ -31,7 +31,8 @@ * @since 1.0 * @version $Revision$ * @author Konsta Vesterinen - * @author Roman Borschel + * @author Roman Borschel + * @todo Rename to DoctrineException */ class Doctrine_Exception extends Exception { diff --git a/lib/Doctrine/Hydrator/RecordDriver.php b/lib/Doctrine/Hydrator/RecordDriver.php index e0688b575..4bf04ebc3 100644 --- a/lib/Doctrine/Hydrator/RecordDriver.php +++ b/lib/Doctrine/Hydrator/RecordDriver.php @@ -72,7 +72,7 @@ class Doctrine_Hydrator_RecordDriver public function initRelatedCollection(Doctrine_Entity $entity, $name) { if ( ! isset($this->_initializedRelations[$entity->getOid()][$name])) { - $relation = $entity->getClassMetadata()->getRelation($name); + $relation = $entity->getClass()->getRelation($name); $relatedClass = $relation->getTable(); $coll = $this->getElementCollection($relatedClass->getClassName()); $coll->setReference($entity, $relation); diff --git a/lib/Doctrine/HydratorNew.php b/lib/Doctrine/HydratorNew.php index 807c399bc..51aed774c 100644 --- a/lib/Doctrine/HydratorNew.php +++ b/lib/Doctrine/HydratorNew.php @@ -519,16 +519,12 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract * $table->prepareValue($field, $value); // Doctrine_Null * * - * @throws Doctrine_Table_Exception if unserialization of array/object typed column fails or - * @throws Doctrine_Table_Exception if uncompression of gzip typed column fails * * @param string $field the name of the field * @param string $value field value * @param string $typeHint A hint on the type of the value. If provided, the type lookup * for the field can be skipped. Used i.e. during hydration to * improve performance on large and/or complex results. * @return mixed prepared value - * @todo To EntityManager. Make private and use in createEntity(). - * .. Or, maybe better: Move to hydrator for performance reasons. */ public function prepareValue(Doctrine_ClassMetadata $class, $fieldName, $value, $typeHint = null) { @@ -540,15 +536,17 @@ class Doctrine_HydratorNew extends Doctrine_Hydrator_Abstract $type = is_null($typeHint) ? $class->getTypeOf($fieldName) : $typeHint; switch ($type) { case 'integer': - case 'string'; - // don't do any casting here PHP INT_MAX is smaller than what the databases support - break; + case 'string': case 'enum': - return $class->enumValue($fieldName, $value); - break; case 'boolean': - return (boolean) $value; + // don't do any conversions on primitive types break; + //case 'enum': + // return $class->enumValue($fieldName, $value); + //break; + //case 'boolean': + // return (boolean) $value; + //break; case 'array': case 'object': if (is_string($value)) { diff --git a/lib/Doctrine/Internal/CommitOrderCalculator.php b/lib/Doctrine/Internal/CommitOrderCalculator.php new file mode 100644 index 000000000..9d345234c --- /dev/null +++ b/lib/Doctrine/Internal/CommitOrderCalculator.php @@ -0,0 +1,110 @@ + + */ +class Doctrine_Internal_CommitOrderCalculator +{ + private $_currentTime; + + /** + * The node list used for sorting. + * + * @var array + */ + private $_nodes = array(); + + /** + * The topologically sorted list of items. Note that these are not nodes + * but the wrapped items. + * + * @var array + */ + private $_sorted; + + /** + * Orders the given list of CommitOrderNodes based on their dependencies. + * + * Uses a depth-first search (DFS) to traverse the graph. + * The desired topological sorting is the reverse postorder of these searches. + * + * @param array $nodes The list of (unordered) CommitOrderNodes. + * @return array The list of ordered items. These are the items wrapped in the nodes. + */ + public function getCommitOrder() + { + // Check whether we need to do anything. 0 or 1 node is easy. + $nodeCount = count($this->_nodes); + if ($nodeCount == 0) { + return array(); + } else if ($nodeCount == 1) { + $node = array_pop($this->_nodes); + return array($node->getClass()); + } + + $this->_sorted = array(); + + // Init + foreach ($this->_nodes as $node) { + $node->markNotVisited(); + $node->setPredecessor(null); + } + + $this->_currentTime = 0; + + // Go + foreach ($this->_nodes as $node) { + if ($node->isNotVisited()) { + $node->visit(); + } + } + + return $this->_sorted; + } + + public function addNode($key, $node) + { + $this->_nodes[$key] = $node; + } + + public function addNodeWithItem($key, $item) + { + $this->_nodes[$key] = new Doctrine_Internal_CommitOrderNode($item, $this); + } + + public function getNodeForKey($key) + { + return $this->_nodes[$key]; + } + + public function hasNodeWithKey($key) + { + return isset($this->_nodes[$key]); + } + + public function clear() + { + $this->_nodes = array(); + $this->_sorted = array(); + } + + + public function getNextTime() + { + return ++$this->_currentTime; + } + + public function prependNode($node) + { + array_unshift($this->_sorted, $node->getClass()); + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/Internal/CommitOrderNode.php b/lib/Doctrine/Internal/CommitOrderNode.php new file mode 100644 index 000000000..92c97908d --- /dev/null +++ b/lib/Doctrine/Internal/CommitOrderNode.php @@ -0,0 +1,136 @@ + + */ +class Doctrine_Internal_CommitOrderNode +{ + const NOT_VISITED = 1; + const IN_PROGRESS = 2; + const VISITED = 3; + + private $_traversalState; + private $_predecessor; + private $_status; + private $_calculator; + private $_relatedNodes = array(); + + private $_discoveryTime; + private $_finishingTime; + + private $_wrappedObj; + private $_relationEdges = array(); + + + public function __construct($wrappedObj, Doctrine_Internal_CommitOrderCalculator $calc) + { + $this->_wrappedObj = $wrappedObj; + $this->_calculator = $calc; + } + + public function getClass() + { + return $this->_wrappedObj; + } + + public function setPredecessor($node) + { + $this->_predecessor = $node; + } + + public function getPredecessor() + { + return $this->_predecessor; + } + + public function markNotVisited() + { + $this->_traversalState = self::NOT_VISITED; + } + + public function markInProgress() + { + $this->_traversalState = self::IN_PROGRESS; + } + + public function markVisited() + { + $this->_traversalState = self::VISITED; + } + + public function isNotVisited() + { + return $this->_traversalState == self::NOT_VISITED; + } + + public function isInProgress() + { + return $this->_traversalState == self::IN_PROGRESS; + } + + public function visit() + { + $this->markInProgress(); + $this->setDiscoveryTime($this->_calculator->getNextTime()); + + foreach ($this->getRelatedNodes() as $node) { + if ($node->isNotVisited()) { + $node->setPredecessor($this); + $node->visit(); + } + if ($node->isInProgress()) { + // back edge => cycle + //TODO: anything to do here? + } + } + + $this->markVisited(); + $this->_calculator->prependNode($this); + $this->setFinishingTime($this->_calculator->getNextTime()); + } + + public function setDiscoveryTime($time) + { + $this->_discoveryTime = $time; + } + + public function setFinishingTime($time) + { + $this->_finishingTime = $time; + } + + public function getDiscoveryTime() + { + return $this->_discoveryTime; + } + + public function getFinishingTime() + { + return $this->_finishingTime; + } + + public function getRelatedNodes() + { + return $this->_relatedNodes; + } + + /** + * Adds a directed dependency (an edge). "$this -before-> $other". + * + * @param Doctrine_Internal_CommitOrderNode $node + */ + public function before(Doctrine_Internal_CommitOrderNode $node) + { + $this->_relatedNodes[] = $node; + } +} + +?> \ No newline at end of file diff --git a/lib/Doctrine/MappingException.php b/lib/Doctrine/MappingException.php index 48933c630..05650a4ee 100644 --- a/lib/Doctrine/MappingException.php +++ b/lib/Doctrine/MappingException.php @@ -12,6 +12,22 @@ class Doctrine_MappingException extends Doctrine_Exception return new self("No identifier specified for Entity '$entityName'." . " Every Entity must have an identifier."); } + + public static function invalidInheritanceType($type) + { + return new self("The inheritance type '$type' does not exist."); + } + + public static function invalidInheritanceOption($name) + { + return new self("The inheritance option '$name' does not exist."); + } + + public static function generatorNotAllowedWithCompositeId() + { + return new self("Id generators can't be used with a composite id."); + } + } ?> \ No newline at end of file diff --git a/lib/Doctrine/Overloadable.php b/lib/Doctrine/Overloadable.php index a879c351d..6ccd3b80f 100644 --- a/lib/Doctrine/Overloadable.php +++ b/lib/Doctrine/Overloadable.php @@ -30,7 +30,7 @@ * @since 1.0 * @version $Revision$ * @author Konsta Vesterinen - * @todo Really needed? + * @todo Really needed? Remove. */ interface Doctrine_Overloadable { /** diff --git a/lib/Doctrine/Tree.php b/lib/Doctrine/Tree.php index 770290c45..73b51378f 100644 --- a/lib/Doctrine/Tree.php +++ b/lib/Doctrine/Tree.php @@ -28,6 +28,7 @@ * @since 1.0 * @version $Revision$ * @author Joe Simms + * @todo Move to NestedSet behavior. */ class Doctrine_Tree { diff --git a/lib/Doctrine/Validator.php b/lib/Doctrine/Validator.php index d043c1da7..702e649a0 100644 --- a/lib/Doctrine/Validator.php +++ b/lib/Doctrine/Validator.php @@ -33,6 +33,7 @@ * @version $Revision$ * @author Roman Borschel * @author Konsta Vesterinen + * @todo Move to validator package. */ class Doctrine_Validator { diff --git a/tests/Orm/Associations/CascadeTest.php b/tests/Orm/Associations/CascadeTest.php new file mode 100644 index 000000000..ffd4d9065 --- /dev/null +++ b/tests/Orm/Associations/CascadeTest.php @@ -0,0 +1,74 @@ +cascade($entity, $container); + } + + +} + +abstract class Cascade +{ + public function cascade(Doctrine_Entity $record, array &$container) + { + if ($this->shouldCascadeTo($record)) { + $container[$record->getOid()] = $record; + } + + foreach ($record->getTable()->getRelations() as $relation) { + if ($this->doCascade($relation)) { + $this->prepareCascade($record, $relation); + $relatedObjects = $record->get($relation->getAlias()); + if ($relatedObjects instanceof Doctrine_Record && $this->shouldCascadeTo($relatedObjects) + && ! isset($container[$relatedObjects->getOid()])) { + $this->cascade($relatedObjects, $container); + } else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) { + foreach ($relatedObjects as $object) { + if ( ! isset($container[$object->getOid()])) { + $this->cascade($object, $container); + } + } + } + } + } + } +} + +class DeleteCascade extends Cascade +{ + public function doCascade($relation) + { + return $relation->isCascadeDelete(); + } + + public function prepareCascade($record, $relation) + { + $fieldName = $relation->getAlias(); + // if it's a xToOne relation and the related object is already loaded + // we don't need to refresh, else we need to. + if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) { + $record->refreshRelated($relation->getAlias()); + } + } + + public function shouldCascadeTo(Doctrine_Entity $entity) + { + //TODO: also ignore removed Entities. incorporate that in exists() with a new + // state? (DELETED?) + return ! $entity->exists(); + } +} \ No newline at end of file diff --git a/tests/Orm/Associations/OneToOneMappingTest.php b/tests/Orm/Associations/OneToOneMappingTest.php new file mode 100644 index 000000000..62f81a1cb --- /dev/null +++ b/tests/Orm/Associations/OneToOneMappingTest.php @@ -0,0 +1,36 @@ + 'address', + 'targetEntity' => 'Address', + 'joinColumns' => array('address_id' => 'id'), + 'sourceEntity' => 'Person' // This is normally filled by ClassMetadata + ); + + $oneToOneMapping = new Doctrine_Association_OneToOne($owningSideMapping); + + $this->assertEquals(array('address_id' => 'id'), $oneToOneMapping->getSourceToTargetKeyColumns()); + $this->assertEquals(array('id' => 'address_id'), $oneToOneMapping->getTargetToSourceKeyColumns()); + $this->assertEquals('Address', $oneToOneMapping->getTargetEntityName()); + $this->assertEquals('Person', $oneToOneMapping->getSourceEntityName()); + $this->assertEquals('address', $oneToOneMapping->getSourceFieldName()); + $this->assertTrue($oneToOneMapping->isOwningSide()); + + + $inverseSideMapping = array( + 'mappedBy' => 'address' + ); + + $oneToOneMapping = new Doctrine_Association_OneToOne($inverseSideMapping); + $this->assertEquals('address', $oneToOneMapping->getMappedByFieldName()); + $this->assertTrue($oneToOneMapping->isInverseSide()); + + } + +} +?> \ No newline at end of file diff --git a/tests/Orm/Component/CollectionTest.php b/tests/Orm/Component/CollectionTest.php index 49e264be1..1fa763e71 100644 --- a/tests/Orm/Component/CollectionTest.php +++ b/tests/Orm/Component/CollectionTest.php @@ -106,17 +106,17 @@ class Orm_Component_CollectionTest extends Doctrine_OrmTestCase /** * @test */ - public function shouldSetKeyColumnWhenAddingNewRowAsArray() + /*public function shouldSetKeyColumnWhenAddingNewRowAsArray() { $this->assertTrue(isset($this->cmsColl['test'])); $this->assertEquals($this->cmsUser, $this->cmsColl['test']); - } + }*/ /** * @test */ - public function shouldSerializeAndUnserializeCollectionWithData() + /*public function shouldSerializeAndUnserializeCollectionWithData() { $serialized = serialize($this->cmsColl); $coll = unserialize($serialized); @@ -126,6 +126,6 @@ class Orm_Component_CollectionTest extends Doctrine_OrmTestCase $user = $coll['test']; $this->assertTrue($user instanceOf CmsUser); $this->assertEquals('test', $user['username']); - } + }*/ } diff --git a/tests/Orm/Entity/AccessorTest.php b/tests/Orm/Entity/AccessorTest.php index 9a95c5552..d3ffdcdbd 100644 --- a/tests/Orm/Entity/AccessorTest.php +++ b/tests/Orm/Entity/AccessorTest.php @@ -22,12 +22,21 @@ class Orm_Entity_AccessorTest extends Doctrine_OrmTestCase class CustomAccessorMutatorTestEntity extends Doctrine_Entity { - public static function initMetadata($class) + public static function initMetadata($mapping) { - $class->mapColumn('id', 'integer', 4, array('primary')); - $class->mapColumn('username', 'string', 50, array( - 'accessor' => 'getUsernameCustom', - 'mutator' => 'setUsernameCustom')); + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true + )); + $mapping->mapField(array( + 'fieldName' => 'username', + 'type' => 'string', + 'length' => 50, + 'accessor' => 'getUsernameCustom', + 'mutator' => 'setUsernameCustom' + )); } public function getUsernameCustom() @@ -43,10 +52,19 @@ class CustomAccessorMutatorTestEntity extends Doctrine_Entity class MagicAccessorMutatorTestEntity extends Doctrine_Entity { - public static function initMetadata($class) + public static function initMetadata($mapping) { - $class->mapColumn('id', 'integer', 4, array('primary')); - $class->mapColumn('username', 'string', 50, array()); + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true + )); + $mapping->mapField(array( + 'fieldName' => 'username', + 'type' => 'string', + 'length' => 50 + )); } public function getUsername() diff --git a/tests/Orm/Entity/ConstructorTest.php b/tests/Orm/Entity/ConstructorTest.php index 137027bd3..85f58dd1e 100644 --- a/tests/Orm/Entity/ConstructorTest.php +++ b/tests/Orm/Entity/ConstructorTest.php @@ -22,10 +22,19 @@ class ConstructorTestEntity1 extends Doctrine_Entity } /* The mapping definition */ - public static function initMetadata($class) + public static function initMetadata($mapping) { - $class->mapColumn('id', 'integer', 4, array('primary')); - $class->mapColumn('username', 'string', 50, array()); + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true + )); + $mapping->mapField(array( + 'fieldName' => 'username', + 'type' => 'string', + 'length' => 50 + )); } } diff --git a/tests/Orm/UnitOfWorkTest.php b/tests/Orm/UnitOfWorkTest.php index 354446d50..650f84eb0 100644 --- a/tests/Orm/UnitOfWorkTest.php +++ b/tests/Orm/UnitOfWorkTest.php @@ -1,59 +1,160 @@ _user = new ForumUser(); - $this->_unitOfWork = new Doctrine_Connection_UnitOfWork($this->_em); + $this->_user->id = 1; + $this->_user->username = 'romanb'; + + $this->_connectionMock = new Doctrine_ConnectionMock(array()); + $this->_sequenceMock = $this->_connectionMock->getSequenceModule(); + $this->_emMock = new Doctrine_EntityManagerMock($this->_connectionMock); + $this->_persisterMock = $this->_emMock->getEntityPersister("ForumUser"); + $this->_unitOfWork = $this->_emMock->getUnitOfWork(); } protected function tearDown() { $this->_user->free(); } + /* Basic registration tests */ + public function testRegisterNew() { - $this->_user->username = 'romanb'; - $this->_user->id = 1; // registerNew() is normally called in save()/persist() $this->_unitOfWork->registerNew($this->_user); $this->assertTrue($this->_unitOfWork->isRegisteredNew($this->_user)); - $this->assertTrue($this->_unitOfWork->contains($this->_user)); + $this->assertTrue($this->_unitOfWork->isInIdentityMap($this->_user)); $this->assertFalse($this->_unitOfWork->isRegisteredDirty($this->_user)); $this->assertFalse($this->_unitOfWork->isRegisteredRemoved($this->_user)); } + /*public function testRegisterNewPerf() { + $s = microtime(true); + + for ($i=1; $i<40000; $i++) { + $user = new ForumUser(); + $user->id = $i; + $this->_unitOfWork->registerNew($user); + } + $e = microtime(true); + + echo $e - $s . " seconds" . PHP_EOL; + }*/ + public function testRegisterDirty() { - $this->_user->username = 'romanb'; - $this->_user->id = 1; - $this->assertEquals(Doctrine_Entity::STATE_TDIRTY, $this->_user->_state()); - $this->assertFalse($this->_unitOfWork->contains($this->_user)); + $this->assertEquals(Doctrine_Entity::STATE_NEW, $this->_user->_state()); + $this->assertFalse($this->_unitOfWork->isInIdentityMap($this->_user)); $this->_unitOfWork->registerDirty($this->_user); $this->assertTrue($this->_unitOfWork->isRegisteredDirty($this->_user)); $this->assertFalse($this->_unitOfWork->isRegisteredNew($this->_user)); $this->assertFalse($this->_unitOfWork->isRegisteredRemoved($this->_user)); } - public function testRegisterRemovedOnTransientEntityIsIgnored() + public function testRegisterRemovedOnNewEntityIsIgnored() { - $this->_user->username = 'romanb'; - $this->_user->id = 1; $this->assertFalse($this->_unitOfWork->isRegisteredRemoved($this->_user)); - $this->_unitOfWork->registerRemoved($this->_user); + $this->_unitOfWork->registerDeleted($this->_user); $this->assertFalse($this->_unitOfWork->isRegisteredRemoved($this->_user)); } - /*public function testSavedEntityHasIdentityAndIsManaged() + + /* Operational tests */ + + public function testSavingSingleEntityWithIdentityColumnForcesInsert() { - $this->_user->username = 'romanb'; - $this->_user->save(); - $this->assertTrue($this->_unitOfWork->hasIdentity($this->_user)); - $this->assertTrue($this->_unitOfWork->isManaged($this->_user)); - }*/ + $this->assertEquals(Doctrine_Entity::STATE_NEW, $this->_user->_state()); + + $this->_unitOfWork->save($this->_user); + + $this->assertEquals(1, count($this->_persisterMock->getInserts())); // insert forced + $this->assertEquals(0, count($this->_persisterMock->getUpdates())); + $this->assertEquals(0, count($this->_persisterMock->getDeletes())); + + $this->assertTrue($this->_unitOfWork->isInIdentityMap($this->_user)); + $this->assertEquals(Doctrine_Entity::STATE_MANAGED, $this->_user->_state()); + + // should no longer be scheduled for insert + $this->assertFalse($this->_unitOfWork->isRegisteredNew($this->_user)); + // should have an id + $this->assertTrue(is_numeric($this->_user->id)); + + // Now lets check whether a subsequence commit() does anything + + $this->_persisterMock->reset(); + + $this->_unitOfWork->commit(); // shouldnt do anything + + // verify that nothing happened + $this->assertEquals(0, count($this->_persisterMock->getInserts())); + $this->assertEquals(0, count($this->_persisterMock->getUpdates())); + $this->assertEquals(0, count($this->_persisterMock->getDeletes())); + } + + public function testSavingSingleEntityWithSequenceIdGeneratorSchedulesInsert() + { + //... + } + + public function testSavingSingleEntityWithTableIdGeneratorSchedulesInsert() + { + //... + } + + public function testSavingSingleEntityWithSingleNaturalIdForcesInsert() + { + //... + } + + public function testSavingSingleEntityWithCompositeIdForcesInsert() + { + //... + } + + public function testSavingEntityGraphWithIdentityColumnsForcesInserts() + { + //... + } + + public function testSavingEntityGraphWithSequencesDelaysInserts() + { + //... + } + + public function testSavingEntityGraphWithNaturalIdsForcesInserts() + { + //... + } + + public function testSavingEntityGraphWithMixedIdGenerationStrategies() + { + //... + } + } \ No newline at end of file diff --git a/tests/lib/mocks/Doctrine_ConnectionMock.php b/tests/lib/mocks/Doctrine_ConnectionMock.php new file mode 100644 index 000000000..c913eb3c2 --- /dev/null +++ b/tests/lib/mocks/Doctrine_ConnectionMock.php @@ -0,0 +1,20 @@ +_sequenceModuleMock) { + $this->_sequenceModuleMock = new Doctrine_SequenceMock($this); + } + return $this->_sequenceModuleMock; + } + +} + +?> \ No newline at end of file diff --git a/tests/lib/mocks/Doctrine_EntityManagerMock.php b/tests/lib/mocks/Doctrine_EntityManagerMock.php new file mode 100644 index 000000000..b68836f49 --- /dev/null +++ b/tests/lib/mocks/Doctrine_EntityManagerMock.php @@ -0,0 +1,25 @@ +_persisterMock) { + $this->_persisterMock = new Doctrine_EntityPersisterMock($this, $this->getClassMetadata($entityName)); + } + return $this->_persisterMock; + } +} + +?> \ No newline at end of file diff --git a/tests/lib/mocks/Doctrine_EntityPersisterMock.php b/tests/lib/mocks/Doctrine_EntityPersisterMock.php new file mode 100644 index 000000000..dec9d2d21 --- /dev/null +++ b/tests/lib/mocks/Doctrine_EntityPersisterMock.php @@ -0,0 +1,56 @@ +getClass()->isIdGeneratorIdentity()) { + $entity->_assignIdentifier($this->_identityColumnValueCounter++); + $this->_em->getUnitOfWork()->addToIdentityMap($entity); + } + + $this->_inserts[] = $entity; + } + + public function update($entity) + { + $this->_updates[] = $entity; + } + + public function delete($entity) + { + $this->_deletes[] = $entity; + } + + public function getInserts() + { + return $this->_inserts; + } + + public function getUpdates() + { + return $this->_updates; + } + + public function getDeletes() + { + return $this->_deletes; + } + + public function reset() + { + $this->_identityColumnValueCounter = 0; + $this->_inserts = array(); + $this->_updates = array(); + $this->_deletes = array(); + } + +} + +?> \ No newline at end of file diff --git a/tests/lib/mocks/Doctrine_SequenceMock.php b/tests/lib/mocks/Doctrine_SequenceMock.php new file mode 100644 index 000000000..7ade2776f --- /dev/null +++ b/tests/lib/mocks/Doctrine_SequenceMock.php @@ -0,0 +1,37 @@ +_sequenceNumber++; + } + + /** + * @override + */ + public function lastInsertId($table = null, $field = null) + { + return $this->_sequenceNumber - 1; + } + + /** + * @override + */ + public function currId($seqName) + { + return $this->_sequenceNumber; + } + + public function reset() + { + $this->_sequenceNumber = 0; + } +} + +?> \ No newline at end of file diff --git a/tests/models/cms/CmsArticle.php b/tests/models/cms/CmsArticle.php index e9dd55541..b9c820e29 100755 --- a/tests/models/cms/CmsArticle.php +++ b/tests/models/cms/CmsArticle.php @@ -1,13 +1,40 @@ mapColumn('id', 'integer', 4, array('primary' => true, 'autoincrement' => true)); - $class->mapColumn('topic', 'string', 255); - $class->mapColumn('text', 'string'); - $class->mapColumn('user_id', 'integer', 4); - $class->hasMany('CmsComment as comments', array( - 'local' => 'id', 'foreign' => 'article_id')); - } + #protected $id; + #protected $topic; + #protected $text; + #protected $user_id; + + public static function initMetadata($mapping) + { + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true, + 'generatorType' => 'auto' + )); + $mapping->mapField(array( + 'fieldName' => 'topic', + 'type' => 'string', + 'length' => 255 + )); + $mapping->mapField(array( + 'fieldName' => 'text', + 'type' => 'string' + )); + $mapping->mapField(array( + 'fieldName' => 'user_id', + 'type' => 'integer', + 'length' => 4 + )); + $mapping->hasMany('CmsComment as comments', array( + 'local' => 'id', 'foreign' => 'article_id')); + } } diff --git a/tests/models/cms/CmsComment.php b/tests/models/cms/CmsComment.php index 56a2c9443..52663b0b9 100755 --- a/tests/models/cms/CmsComment.php +++ b/tests/models/cms/CmsComment.php @@ -1,11 +1,38 @@ mapColumn('id', 'integer', 4, array('primary' => true, 'autoincrement' => true)); - $class->mapColumn('topic', 'string', 255); - $class->mapColumn('text', 'string'); - $class->mapColumn('article_id', 'integer', 4); - } + #protected $id; + #protected $topic; + #protected $text; + #protected $article_id; + + public static function initMetadata($mapping) + { + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true, + 'generatorType' => 'auto' + )); + $mapping->mapField(array( + 'fieldName' => 'topic', + 'type' => 'string', + 'length' => 255 + )); + $mapping->mapField(array( + 'fieldName' => 'text', + 'type' => 'string' + )); + $mapping->mapField(array( + 'fieldName' => 'article_id', + 'type' => 'integer', + 'length' => 4 + )); + } } diff --git a/tests/models/cms/CmsPhonenumber.php b/tests/models/cms/CmsPhonenumber.php index ceb46ee8e..6e7d22a3d 100755 --- a/tests/models/cms/CmsPhonenumber.php +++ b/tests/models/cms/CmsPhonenumber.php @@ -1,9 +1,21 @@ mapColumn('user_id', 'integer', 4); - $class->mapColumn('phonenumber', 'string', 50, array('primary' => true)); - } + #protected $user_id; + #protected $phonenumber; + + public static function initMetadata($mapping) + { + $mapping->mapField(array( + 'fieldName' => 'user_id', + 'type' => 'integer', + 'length' => 4 + )); + $mapping->mapField(array( + 'fieldName' => 'phonenumber', + 'type' => 'string', + 'length' => 50, + 'id' => true + )); + } } diff --git a/tests/models/cms/CmsUser.php b/tests/models/cms/CmsUser.php index 760049975..146dad6f7 100644 --- a/tests/models/cms/CmsUser.php +++ b/tests/models/cms/CmsUser.php @@ -1,16 +1,44 @@ mapColumn('id', 'integer', 4, array('primary' => true, 'autoincrement' => true)); - $class->mapColumn('status', 'string', 50); - $class->mapColumn('username', 'string', 255); - $class->mapColumn('name', 'string', 255); - - $class->hasMany('CmsPhonenumber as phonenumbers', array( + #protected $id; + #protected $status; + #protected $username; + #protected $name; + + public static function initMetadata($mapping) + { + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true, + 'generatorType' => 'auto' + )); + $mapping->mapField(array( + 'fieldName' => 'status', + 'type' => 'string', + 'length' => 50 + )); + $mapping->mapField(array( + 'fieldName' => 'username', + 'type' => 'string', + 'length' => 255 + )); + $mapping->mapField(array( + 'fieldName' => 'name', + 'type' => 'string', + 'length' => 255 + )); + + $mapping->hasMany('CmsPhonenumber as phonenumbers', array( 'local' => 'id', 'foreign' => 'user_id')); - $class->hasMany('CmsArticle as articles', array( + $mapping->hasMany('CmsArticle as articles', array( 'local' => 'id', 'foreign' => 'user_id')); - } + } } diff --git a/tests/models/forum/ForumAdministrator.php b/tests/models/forum/ForumAdministrator.php index 00884d540..d69f31ba6 100644 --- a/tests/models/forum/ForumAdministrator.php +++ b/tests/models/forum/ForumAdministrator.php @@ -2,9 +2,14 @@ class ForumAdministrator extends ForumUser { - public static function initMetadata($class) + public static function initMetadata($mapping) { - $class->mapColumn('access_level as accessLevel', 'integer', 1); + $mapping->mapField(array( + 'fieldName' => 'accessLevel', + 'columnName' => 'access_level', + 'type' => 'integer', + 'length' => 1 + )); } public function banUser(ForumUser $user) {} diff --git a/tests/models/forum/ForumBoard.php b/tests/models/forum/ForumBoard.php index 11dc974ca..1b176cec1 100755 --- a/tests/models/forum/ForumBoard.php +++ b/tests/models/forum/ForumBoard.php @@ -1,6 +1,8 @@ mapField(array( 'fieldName' => 'id', 'id' => true, @@ -8,10 +10,22 @@ class ForumBoard extends Doctrine_Entity { 'length' => 4 )); */ - $metadata->mapColumn('id', 'integer', 4, array('primary')); - $metadata->mapColumn('position', 'integer'); - $metadata->mapColumn('category_id', 'integer'); - $metadata->hasOne('ForumCategory as category', + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true + )); + $mapping->mapField(array( + 'fieldName' => 'position', + 'type' => 'integer' + )); + $mapping->mapField(array( + 'fieldName' => 'category_id', + 'type' => 'integer' + )); + + $mapping->hasOne('ForumCategory as category', array('local' => 'category_id', 'foreign' => 'id')); /* $metadata->mapOneToOne(array( diff --git a/tests/models/forum/ForumCategory.php b/tests/models/forum/ForumCategory.php index 0b1bbb340..0021ba6ad 100755 --- a/tests/models/forum/ForumCategory.php +++ b/tests/models/forum/ForumCategory.php @@ -1,10 +1,26 @@ mapColumn('id', 'integer', 4, array('primary')); - $class->mapColumn('position', 'integer'); - $class->mapColumn('name', 'string', 255); - $class->hasMany('ForumBoard as boards', array( +class ForumCategory extends Doctrine_Entity +{ + + public static function initMetadata($mapping) + { + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true + )); + $mapping->mapField(array( + 'fieldName' => 'position', + 'type' => 'integer' + )); + $mapping->mapField(array( + 'fieldName' => 'name', + 'type' => 'string', + 'length' => 255 + )); + + $mapping->hasMany('ForumBoard as boards', array( 'local' => 'id' , 'foreign' => 'category_id')); } } diff --git a/tests/models/forum/ForumEntry.php b/tests/models/forum/ForumEntry.php new file mode 100644 index 000000000..f75e97755 --- /dev/null +++ b/tests/models/forum/ForumEntry.php @@ -0,0 +1,32 @@ +mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true, + 'generatorType' => 'auto' + )); + $mapping->mapField(array( + 'fieldName' => 'topic', + 'type' => 'string', + 'length' => 50 + )); + + } + + +} + +?> \ No newline at end of file diff --git a/tests/models/forum/ForumUser.php b/tests/models/forum/ForumUser.php index 73b68495b..6d4c18eb9 100644 --- a/tests/models/forum/ForumUser.php +++ b/tests/models/forum/ForumUser.php @@ -1,26 +1,46 @@ setInheritanceType(Doctrine::INHERITANCE_TYPE_JOINED, array( + $mapping->setInheritanceType('joined', array( 'discriminatorColumn' => 'dtype', 'discriminatorMap' => array( 'user' => 'ForumUser', 'admin' => 'ForumAdministrator') )); // register subclasses - $class->setSubclasses(array('ForumAdministrator')); + $mapping->setSubclasses(array('ForumAdministrator')); // the discriminator column - $class->mapColumn('dtype', 'string', 50); + $mapping->mapField(array( + 'fieldName' => 'dtype', + 'type' => 'string', + 'length' => 50 + )); // column-to-field mapping - $class->mapColumn('id', 'integer', 4, array( - 'primary' => true, - 'autoincrement' => true)); - $class->mapColumn('username', 'string', 50, array()); + $mapping->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'length' => 4, + 'id' => true, + 'generatorType' => 'auto' + )); + $mapping->mapField(array( + 'fieldName' => 'username', + 'type' => 'string', + 'length' => 50 + )); }