From 722cb6c00844273337dba567976c92d32bc98c8d Mon Sep 17 00:00:00 2001 From: APTX Date: Mon, 10 Jan 2011 03:14:29 +0100 Subject: [PATCH 1/1] Server --- Doctrine/Common/Annotations/Annotation.php | 81 + .../Annotations/AnnotationException.php | 54 + .../Common/Annotations/AnnotationReader.php | 248 ++ Doctrine/Common/Annotations/Lexer.php | 157 + Doctrine/Common/Annotations/Parser.php | 545 ++++ Doctrine/Common/Cache/AbstractCache.php | 226 ++ Doctrine/Common/Cache/ApcCache.php | 90 + Doctrine/Common/Cache/ArrayCache.php | 91 + Doctrine/Common/Cache/Cache.php | 71 + Doctrine/Common/Cache/MemcacheCache.php | 123 + Doctrine/Common/Cache/XcacheCache.php | 105 + Doctrine/Common/ClassLoader.php | 240 ++ .../Common/Collections/ArrayCollection.php | 438 +++ Doctrine/Common/Collections/Collection.php | 243 ++ Doctrine/Common/CommonException.php | 28 + Doctrine/Common/EventArgs.php | 69 + Doctrine/Common/EventManager.php | 138 + Doctrine/Common/EventSubscriber.php | 45 + Doctrine/Common/Lexer.php | 255 ++ Doctrine/Common/NotifyPropertyChanged.php | 45 + Doctrine/Common/PropertyChangedListener.php | 48 + Doctrine/Common/Util/Debug.php | 136 + Doctrine/Common/Util/Inflector.php | 72 + Doctrine/Common/Version.php | 55 + Doctrine/DBAL/Configuration.php | 64 + Doctrine/DBAL/Connection.php | 1052 +++++++ Doctrine/DBAL/ConnectionException.php | 54 + Doctrine/DBAL/DBALException.php | 88 + Doctrine/DBAL/Driver.php | 72 + Doctrine/DBAL/Driver/Connection.php | 42 + Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php | 115 + Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php | 108 + Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php | 27 + Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php | 297 ++ Doctrine/DBAL/Driver/OCI8/Driver.php | 95 + Doctrine/DBAL/Driver/OCI8/OCI8Connection.php | 160 + Doctrine/DBAL/Driver/OCI8/OCI8Exception.php | 30 + Doctrine/DBAL/Driver/OCI8/OCI8Statement.php | 220 ++ Doctrine/DBAL/Driver/PDOConnection.php | 40 + Doctrine/DBAL/Driver/PDOIbm/Driver.php | 126 + Doctrine/DBAL/Driver/PDOMySql/Driver.php | 95 + Doctrine/DBAL/Driver/PDOOracle/Driver.php | 88 + Doctrine/DBAL/Driver/PDOPgSql/Driver.php | 70 + Doctrine/DBAL/Driver/PDOSqlite/Driver.php | 116 + Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php | 45 + Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php | 86 + Doctrine/DBAL/Driver/PDOStatement.php | 33 + Doctrine/DBAL/Driver/Statement.php | 200 ++ Doctrine/DBAL/DriverManager.php | 161 + Doctrine/DBAL/Event/ConnectionEventArgs.php | 79 + .../DBAL/Event/Listeners/MysqlSessionInit.php | 75 + .../Event/Listeners/OracleSessionInit.php | 82 + Doctrine/DBAL/Events.php | 38 + Doctrine/DBAL/LockMode.php | 42 + Doctrine/DBAL/Logging/DebugStack.php | 65 + Doctrine/DBAL/Logging/EchoSQLLogger.php | 61 + Doctrine/DBAL/Logging/SQLLogger.php | 54 + Doctrine/DBAL/Platforms/AbstractPlatform.php | 2047 ++++++++++++ Doctrine/DBAL/Platforms/DB2Platform.php | 564 ++++ Doctrine/DBAL/Platforms/MsSqlPlatform.php | 788 +++++ Doctrine/DBAL/Platforms/MySqlPlatform.php | 602 ++++ Doctrine/DBAL/Platforms/OraclePlatform.php | 724 +++++ .../DBAL/Platforms/PostgreSqlPlatform.php | 710 +++++ Doctrine/DBAL/Platforms/SqlitePlatform.php | 474 +++ Doctrine/DBAL/README.markdown | 0 Doctrine/DBAL/Schema/AbstractAsset.php | 121 + .../DBAL/Schema/AbstractSchemaManager.php | 777 +++++ Doctrine/DBAL/Schema/Column.php | 346 +++ Doctrine/DBAL/Schema/ColumnDiff.php | 58 + Doctrine/DBAL/Schema/Comparator.php | 367 +++ Doctrine/DBAL/Schema/Constraint.php | 38 + Doctrine/DBAL/Schema/DB2SchemaManager.php | 186 ++ Doctrine/DBAL/Schema/ForeignKeyConstraint.php | 164 + Doctrine/DBAL/Schema/Index.php | 176 ++ Doctrine/DBAL/Schema/MsSqlSchemaManager.php | 172 + Doctrine/DBAL/Schema/MySqlSchemaManager.php | 191 ++ Doctrine/DBAL/Schema/OracleSchemaManager.php | 280 ++ .../DBAL/Schema/PostgreSqlSchemaManager.php | 275 ++ Doctrine/DBAL/Schema/Schema.php | 327 ++ Doctrine/DBAL/Schema/SchemaConfig.php | 76 + Doctrine/DBAL/Schema/SchemaDiff.php | 177 ++ Doctrine/DBAL/Schema/SchemaException.php | 126 + Doctrine/DBAL/Schema/Sequence.php | 77 + Doctrine/DBAL/Schema/SqliteSchemaManager.php | 181 ++ Doctrine/DBAL/Schema/Table.php | 619 ++++ Doctrine/DBAL/Schema/TableDiff.php | 139 + Doctrine/DBAL/Schema/View.php | 53 + .../Visitor/CreateSchemaSqlCollector.php | 147 + .../Schema/Visitor/DropSchemaSqlCollector.php | 142 + Doctrine/DBAL/Schema/Visitor/Visitor.php | 75 + Doctrine/DBAL/Statement.php | 237 ++ .../Tools/Console/Command/ImportCommand.php | 127 + .../Tools/Console/Command/RunSqlCommand.php | 86 + .../Tools/Console/Helper/ConnectionHelper.php | 74 + Doctrine/DBAL/Types/ArrayType.php | 59 + Doctrine/DBAL/Types/BigIntType.php | 48 + Doctrine/DBAL/Types/BooleanType.php | 57 + Doctrine/DBAL/Types/ConversionException.php | 48 + Doctrine/DBAL/Types/DateTimeType.php | 59 + Doctrine/DBAL/Types/DateTimeTzType.php | 79 + Doctrine/DBAL/Types/DateType.php | 59 + Doctrine/DBAL/Types/DecimalType.php | 45 + Doctrine/DBAL/Types/FloatType.php | 54 + Doctrine/DBAL/Types/IntegerType.php | 53 + Doctrine/DBAL/Types/ObjectType.php | 40 + Doctrine/DBAL/Types/SmallIntType.php | 52 + Doctrine/DBAL/Types/StringType.php | 50 + Doctrine/DBAL/Types/TextType.php | 56 + Doctrine/DBAL/Types/TimeType.php | 68 + Doctrine/DBAL/Types/Type.php | 230 ++ Doctrine/DBAL/Types/VarDateTimeType.php | 60 + Doctrine/DBAL/Version.php | 55 + Doctrine/ORM/AbstractQuery.php | 603 ++++ Doctrine/ORM/Configuration.php | 486 +++ Doctrine/ORM/EntityManager.php | 729 +++++ Doctrine/ORM/EntityNotFoundException.php | 34 + Doctrine/ORM/EntityRepository.php | 225 ++ Doctrine/ORM/Event/LifecycleEventArgs.php | 60 + .../ORM/Event/LoadClassMetadataEventArgs.php | 54 + Doctrine/ORM/Event/OnFlushEventArgs.php | 78 + Doctrine/ORM/Event/PreUpdateEventArgs.php | 98 + Doctrine/ORM/Events.php | 122 + Doctrine/ORM/Id/AbstractIdGenerator.php | 48 + Doctrine/ORM/Id/AssignedGenerator.php | 69 + Doctrine/ORM/Id/IdentityGenerator.php | 59 + Doctrine/ORM/Id/SequenceGenerator.php | 103 + Doctrine/ORM/Id/TableGenerator.php | 79 + .../ORM/Internal/CommitOrderCalculator.php | 118 + .../Internal/Hydration/AbstractHydrator.php | 278 ++ .../ORM/Internal/Hydration/ArrayHydrator.php | 235 ++ .../Internal/Hydration/HydrationException.php | 17 + .../ORM/Internal/Hydration/IterableResult.php | 104 + .../ORM/Internal/Hydration/ObjectHydrator.php | 429 +++ .../ORM/Internal/Hydration/ScalarHydrator.php | 50 + .../Hydration/SingleScalarHydrator.php | 49 + Doctrine/ORM/Mapping/ClassMetadata.php | 376 +++ Doctrine/ORM/Mapping/ClassMetadataFactory.php | 455 +++ Doctrine/ORM/Mapping/ClassMetadataInfo.php | 1595 ++++++++++ .../ORM/Mapping/Driver/AbstractFileDriver.php | 210 ++ .../ORM/Mapping/Driver/AnnotationDriver.php | 489 +++ .../ORM/Mapping/Driver/DatabaseDriver.php | 276 ++ .../Mapping/Driver/DoctrineAnnotations.php | 137 + Doctrine/ORM/Mapping/Driver/Driver.php | 59 + Doctrine/ORM/Mapping/Driver/DriverChain.php | 116 + Doctrine/ORM/Mapping/Driver/PHPDriver.php | 69 + .../ORM/Mapping/Driver/StaticPHPDriver.php | 122 + Doctrine/ORM/Mapping/Driver/XmlDriver.php | 495 +++ Doctrine/ORM/Mapping/Driver/YamlDriver.php | 455 +++ Doctrine/ORM/Mapping/MappingException.php | 230 ++ Doctrine/ORM/NativeQuery.php | 73 + Doctrine/ORM/NoResultException.php | 34 + Doctrine/ORM/NonUniqueResultException.php | 28 + Doctrine/ORM/ORMException.php | 118 + Doctrine/ORM/OptimisticLockException.php | 63 + Doctrine/ORM/PersistentCollection.php | 681 ++++ .../AbstractCollectionPersister.php | 166 + .../AbstractEntityInheritancePersister.php | 107 + .../ORM/Persisters/BasicEntityPersister.php | 1201 +++++++ .../Persisters/ElementCollectionPersister.php | 33 + .../Persisters/JoinedSubclassPersister.php | 425 +++ .../ORM/Persisters/ManyToManyPersister.php | 176 ++ .../ORM/Persisters/OneToManyPersister.php | 119 + .../ORM/Persisters/SingleTablePersister.php | 118 + .../ORM/Persisters/UnionSubclassPersister.php | 8 + Doctrine/ORM/PessimisticLockException.php | 40 + Doctrine/ORM/Proxy/Proxy.php | 30 + Doctrine/ORM/Proxy/ProxyException.php | 43 + Doctrine/ORM/Proxy/ProxyFactory.php | 289 ++ Doctrine/ORM/Query.php | 562 ++++ Doctrine/ORM/Query/AST/ASTException.php | 13 + .../ORM/Query/AST/AggregateExpression.php | 52 + .../ORM/Query/AST/ArithmeticExpression.php | 54 + Doctrine/ORM/Query/AST/ArithmeticFactor.php | 67 + Doctrine/ORM/Query/AST/ArithmeticTerm.php | 48 + Doctrine/ORM/Query/AST/BetweenExpression.php | 54 + .../Query/AST/CollectionMemberExpression.php | 52 + .../ORM/Query/AST/ComparisonExpression.php | 57 + .../ORM/Query/AST/ConditionalExpression.php | 48 + Doctrine/ORM/Query/AST/ConditionalFactor.php | 49 + Doctrine/ORM/Query/AST/ConditionalPrimary.php | 54 + Doctrine/ORM/Query/AST/ConditionalTerm.php | 48 + Doctrine/ORM/Query/AST/DeleteClause.php | 50 + Doctrine/ORM/Query/AST/DeleteStatement.php | 49 + .../EmptyCollectionComparisonExpression.php | 50 + Doctrine/ORM/Query/AST/ExistsExpression.php | 50 + Doctrine/ORM/Query/AST/FromClause.php | 48 + .../ORM/Query/AST/Functions/AbsFunction.php | 62 + .../Query/AST/Functions/ConcatFunction.php | 67 + .../AST/Functions/CurrentDateFunction.php | 54 + .../AST/Functions/CurrentTimeFunction.php | 54 + .../Functions/CurrentTimestampFunction.php | 54 + .../ORM/Query/AST/Functions/FunctionNode.php | 52 + .../Query/AST/Functions/LengthFunction.php | 62 + .../Query/AST/Functions/LocateFunction.php | 81 + .../ORM/Query/AST/Functions/LowerFunction.php | 62 + .../ORM/Query/AST/Functions/ModFunction.php | 68 + .../ORM/Query/AST/Functions/SizeFunction.php | 122 + .../ORM/Query/AST/Functions/SqrtFunction.php | 61 + .../Query/AST/Functions/SubstringFunction.php | 82 + .../ORM/Query/AST/Functions/TrimFunction.php | 100 + .../ORM/Query/AST/Functions/UpperFunction.php | 62 + Doctrine/ORM/Query/AST/GroupByClause.php | 48 + Doctrine/ORM/Query/AST/HavingClause.php | 48 + .../AST/IdentificationVariableDeclaration.php | 52 + Doctrine/ORM/Query/AST/InExpression.php | 52 + Doctrine/ORM/Query/AST/IndexBy.php | 48 + Doctrine/ORM/Query/AST/InputParameter.php | 58 + .../ORM/Query/AST/InstanceOfExpression.php | 49 + Doctrine/ORM/Query/AST/Join.php | 58 + .../AST/JoinAssociationPathExpression.php | 50 + .../ORM/Query/AST/JoinVariableDeclaration.php | 50 + Doctrine/ORM/Query/AST/LikeExpression.php | 53 + Doctrine/ORM/Query/AST/Literal.php | 24 + Doctrine/ORM/Query/AST/Node.php | 98 + .../Query/AST/NullComparisonExpression.php | 50 + Doctrine/ORM/Query/AST/OrderByClause.php | 48 + Doctrine/ORM/Query/AST/OrderByItem.php | 59 + .../ORM/Query/AST/PartialObjectExpression.php | 15 + Doctrine/ORM/Query/AST/PathExpression.php | 58 + .../ORM/Query/AST/QuantifiedExpression.php | 68 + .../Query/AST/RangeVariableDeclaration.php | 50 + Doctrine/ORM/Query/AST/SelectClause.php | 50 + Doctrine/ORM/Query/AST/SelectExpression.php | 51 + Doctrine/ORM/Query/AST/SelectStatement.php | 53 + .../Query/AST/SimpleArithmeticExpression.php | 48 + Doctrine/ORM/Query/AST/SimpleSelectClause.php | 50 + .../ORM/Query/AST/SimpleSelectExpression.php | 50 + Doctrine/ORM/Query/AST/Subselect.php | 54 + .../ORM/Query/AST/SubselectFromClause.php | 48 + Doctrine/ORM/Query/AST/UpdateClause.php | 52 + Doctrine/ORM/Query/AST/UpdateItem.php | 53 + Doctrine/ORM/Query/AST/UpdateStatement.php | 49 + Doctrine/ORM/Query/AST/WhereClause.php | 48 + .../ORM/Query/Exec/AbstractSqlExecutor.php | 57 + .../Query/Exec/MultiTableDeleteExecutor.php | 129 + .../Query/Exec/MultiTableUpdateExecutor.php | 161 + .../ORM/Query/Exec/SingleSelectExecutor.php | 48 + .../Exec/SingleTableDeleteUpdateExecutor.php | 53 + Doctrine/ORM/Query/Expr.php | 569 ++++ Doctrine/ORM/Query/Expr/Andx.php | 43 + Doctrine/ORM/Query/Expr/Base.php | 85 + Doctrine/ORM/Query/Expr/Comparison.php | 59 + Doctrine/ORM/Query/Expr/From.php | 60 + Doctrine/ORM/Query/Expr/Func.php | 50 + Doctrine/ORM/Query/Expr/GroupBy.php | 39 + Doctrine/ORM/Query/Expr/Join.php | 64 + Doctrine/ORM/Query/Expr/Literal.php | 9 + Doctrine/ORM/Query/Expr/Math.php | 66 + Doctrine/ORM/Query/Expr/OrderBy.php | 66 + Doctrine/ORM/Query/Expr/Orx.php | 43 + Doctrine/ORM/Query/Expr/Select.php | 42 + Doctrine/ORM/Query/Lexer.php | 196 ++ Doctrine/ORM/Query/Parser.php | 2764 +++++++++++++++++ Doctrine/ORM/Query/ParserResult.php | 141 + Doctrine/ORM/Query/Printer.php | 95 + Doctrine/ORM/Query/QueryException.php | 139 + Doctrine/ORM/Query/ResultSetMapping.php | 396 +++ Doctrine/ORM/Query/SqlWalker.php | 1835 +++++++++++ Doctrine/ORM/Query/TreeWalker.php | 403 +++ Doctrine/ORM/Query/TreeWalkerAdapter.php | 437 +++ Doctrine/ORM/Query/TreeWalkerChain.php | 651 ++++ Doctrine/ORM/QueryBuilder.php | 955 ++++++ Doctrine/ORM/README.markdown | 0 .../Command/ClearCache/MetadataCommand.php | 85 + .../Command/ClearCache/QueryCommand.php | 85 + .../Command/ClearCache/ResultCommand.php | 168 + .../Command/ConvertDoctrine1SchemaCommand.php | 223 ++ .../Console/Command/ConvertMappingCommand.php | 151 + .../EnsureProductionSettingsCommand.php | 85 + .../Command/GenerateEntitiesCommand.php | 146 + .../Command/GenerateProxiesCommand.php | 115 + .../Command/GenerateRepositoriesCommand.php | 116 + .../Tools/Console/Command/RunDqlCommand.php | 124 + .../Command/SchemaTool/AbstractCommand.php | 64 + .../Command/SchemaTool/CreateCommand.php | 79 + .../Command/SchemaTool/DropCommand.php | 111 + .../Command/SchemaTool/UpdateCommand.php | 103 + .../Console/Command/ValidateSchemaCommand.php | 89 + Doctrine/ORM/Tools/Console/ConsoleRunner.php | 69 + .../Console/Helper/EntityManagerHelper.php | 74 + Doctrine/ORM/Tools/Console/MetadataFilter.php | 80 + Doctrine/ORM/Tools/ConvertDoctrine1Schema.php | 274 ++ .../DisconnectedClassMetadataFactory.php | 62 + Doctrine/ORM/Tools/EntityGenerator.php | 944 ++++++ .../ORM/Tools/EntityRepositoryGenerator.php | 83 + .../Tools/Event/GenerateSchemaEventArgs.php | 65 + .../Event/GenerateSchemaTableEventArgs.php | 75 + .../Tools/Export/ClassMetadataExporter.php | 76 + .../Tools/Export/Driver/AbstractExporter.php | 205 ++ .../Export/Driver/AnnotationExporter.php | 72 + .../ORM/Tools/Export/Driver/PhpExporter.php | 161 + .../ORM/Tools/Export/Driver/XmlExporter.php | 323 ++ .../ORM/Tools/Export/Driver/YamlExporter.php | 198 ++ Doctrine/ORM/Tools/Export/ExportException.php | 18 + Doctrine/ORM/Tools/SchemaTool.php | 679 ++++ Doctrine/ORM/Tools/SchemaValidator.php | 219 ++ Doctrine/ORM/Tools/ToolEvents.php | 44 + Doctrine/ORM/Tools/ToolsException.php | 13 + Doctrine/ORM/TransactionRequiredException.php | 40 + Doctrine/ORM/UnitOfWork.php | 2262 ++++++++++++++ Doctrine/ORM/Version.php | 55 + .../Symfony/Component/Console/Application.php | 743 +++++ .../Component/Console/Command/Command.php | 512 +++ .../Component/Console/Command/HelpCommand.php | 77 + .../Component/Console/Command/ListCommand.php | 67 + .../Component/Console/Helper/DialogHelper.php | 110 + .../Console/Helper/FormatterHelper.php | 82 + .../Component/Console/Helper/Helper.php | 42 + .../Console/Helper/HelperInterface.php | 41 + .../Component/Console/Helper/HelperSet.php | 102 + .../Component/Console/Input/ArgvInput.php | 255 ++ .../Component/Console/Input/ArrayInput.php | 162 + .../Symfony/Component/Console/Input/Input.php | 199 ++ .../Component/Console/Input/InputArgument.php | 128 + .../Console/Input/InputDefinition.php | 471 +++ .../Console/Input/InputInterface.php | 56 + .../Component/Console/Input/InputOption.php | 178 ++ .../Component/Console/Input/StringInput.php | 71 + .../Console/Output/ConsoleOutput.php | 39 + .../Component/Console/Output/NullOutput.php | 32 + .../Component/Console/Output/Output.php | 231 ++ .../Console/Output/OutputInterface.php | 45 + .../Component/Console/Output/StreamOutput.php | 105 + Doctrine/Symfony/Component/Console/Shell.php | 136 + .../Console/Tester/ApplicationTester.php | 101 + .../Console/Tester/CommandTester.php | 101 + Doctrine/Symfony/Component/Yaml/Dumper.php | 59 + Doctrine/Symfony/Component/Yaml/Exception.php | 23 + Doctrine/Symfony/Component/Yaml/Inline.php | 410 +++ Doctrine/Symfony/Component/Yaml/Parser.php | 587 ++++ .../Component/Yaml/ParserException.php | 23 + Doctrine/Symfony/Component/Yaml/Yaml.php | 121 + Proxies/ScoreProxy.php | 35 + Proxies/SessionProxy.php | 35 + Proxies/UserProxy.php | 89 + Tim/Score.php | 33 + Tim/Session.php | 44 + Tim/User.php | 64 + chatserver.php | 146 + config/mappings/Score.dcm.xml | 17 + config/mappings/Session.dcm.xml | 18 + config/mappings/User.dcm.xml | 19 + doctrine-orm/LICENSE | 504 +++ doctrine-orm/bin/doctrine | 4 + doctrine-orm/bin/doctrine.bat | 9 + doctrine-orm/bin/doctrine.php | 50 + doctrine.php | 10 + index.php | 148 + init.php | 45 + 349 files changed, 61600 insertions(+) create mode 100644 Doctrine/Common/Annotations/Annotation.php create mode 100644 Doctrine/Common/Annotations/AnnotationException.php create mode 100644 Doctrine/Common/Annotations/AnnotationReader.php create mode 100644 Doctrine/Common/Annotations/Lexer.php create mode 100644 Doctrine/Common/Annotations/Parser.php create mode 100644 Doctrine/Common/Cache/AbstractCache.php create mode 100644 Doctrine/Common/Cache/ApcCache.php create mode 100644 Doctrine/Common/Cache/ArrayCache.php create mode 100644 Doctrine/Common/Cache/Cache.php create mode 100644 Doctrine/Common/Cache/MemcacheCache.php create mode 100644 Doctrine/Common/Cache/XcacheCache.php create mode 100644 Doctrine/Common/ClassLoader.php create mode 100644 Doctrine/Common/Collections/ArrayCollection.php create mode 100644 Doctrine/Common/Collections/Collection.php create mode 100644 Doctrine/Common/CommonException.php create mode 100644 Doctrine/Common/EventArgs.php create mode 100644 Doctrine/Common/EventManager.php create mode 100644 Doctrine/Common/EventSubscriber.php create mode 100644 Doctrine/Common/Lexer.php create mode 100644 Doctrine/Common/NotifyPropertyChanged.php create mode 100644 Doctrine/Common/PropertyChangedListener.php create mode 100644 Doctrine/Common/Util/Debug.php create mode 100644 Doctrine/Common/Util/Inflector.php create mode 100644 Doctrine/Common/Version.php create mode 100644 Doctrine/DBAL/Configuration.php create mode 100644 Doctrine/DBAL/Connection.php create mode 100644 Doctrine/DBAL/ConnectionException.php create mode 100644 Doctrine/DBAL/DBALException.php create mode 100644 Doctrine/DBAL/Driver.php create mode 100644 Doctrine/DBAL/Driver/Connection.php create mode 100644 Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php create mode 100644 Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php create mode 100644 Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php create mode 100644 Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php create mode 100644 Doctrine/DBAL/Driver/OCI8/Driver.php create mode 100644 Doctrine/DBAL/Driver/OCI8/OCI8Connection.php create mode 100644 Doctrine/DBAL/Driver/OCI8/OCI8Exception.php create mode 100644 Doctrine/DBAL/Driver/OCI8/OCI8Statement.php create mode 100644 Doctrine/DBAL/Driver/PDOConnection.php create mode 100644 Doctrine/DBAL/Driver/PDOIbm/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOMySql/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOOracle/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOPgSql/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOSqlite/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php create mode 100644 Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php create mode 100644 Doctrine/DBAL/Driver/PDOStatement.php create mode 100644 Doctrine/DBAL/Driver/Statement.php create mode 100644 Doctrine/DBAL/DriverManager.php create mode 100644 Doctrine/DBAL/Event/ConnectionEventArgs.php create mode 100644 Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php create mode 100644 Doctrine/DBAL/Event/Listeners/OracleSessionInit.php create mode 100644 Doctrine/DBAL/Events.php create mode 100644 Doctrine/DBAL/LockMode.php create mode 100644 Doctrine/DBAL/Logging/DebugStack.php create mode 100644 Doctrine/DBAL/Logging/EchoSQLLogger.php create mode 100644 Doctrine/DBAL/Logging/SQLLogger.php create mode 100644 Doctrine/DBAL/Platforms/AbstractPlatform.php create mode 100644 Doctrine/DBAL/Platforms/DB2Platform.php create mode 100644 Doctrine/DBAL/Platforms/MsSqlPlatform.php create mode 100644 Doctrine/DBAL/Platforms/MySqlPlatform.php create mode 100644 Doctrine/DBAL/Platforms/OraclePlatform.php create mode 100644 Doctrine/DBAL/Platforms/PostgreSqlPlatform.php create mode 100644 Doctrine/DBAL/Platforms/SqlitePlatform.php create mode 100644 Doctrine/DBAL/README.markdown create mode 100644 Doctrine/DBAL/Schema/AbstractAsset.php create mode 100644 Doctrine/DBAL/Schema/AbstractSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/Column.php create mode 100644 Doctrine/DBAL/Schema/ColumnDiff.php create mode 100644 Doctrine/DBAL/Schema/Comparator.php create mode 100644 Doctrine/DBAL/Schema/Constraint.php create mode 100644 Doctrine/DBAL/Schema/DB2SchemaManager.php create mode 100644 Doctrine/DBAL/Schema/ForeignKeyConstraint.php create mode 100644 Doctrine/DBAL/Schema/Index.php create mode 100644 Doctrine/DBAL/Schema/MsSqlSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/MySqlSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/OracleSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/Schema.php create mode 100644 Doctrine/DBAL/Schema/SchemaConfig.php create mode 100644 Doctrine/DBAL/Schema/SchemaDiff.php create mode 100644 Doctrine/DBAL/Schema/SchemaException.php create mode 100644 Doctrine/DBAL/Schema/Sequence.php create mode 100644 Doctrine/DBAL/Schema/SqliteSchemaManager.php create mode 100644 Doctrine/DBAL/Schema/Table.php create mode 100644 Doctrine/DBAL/Schema/TableDiff.php create mode 100644 Doctrine/DBAL/Schema/View.php create mode 100644 Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php create mode 100644 Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php create mode 100644 Doctrine/DBAL/Schema/Visitor/Visitor.php create mode 100644 Doctrine/DBAL/Statement.php create mode 100644 Doctrine/DBAL/Tools/Console/Command/ImportCommand.php create mode 100644 Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php create mode 100644 Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php create mode 100644 Doctrine/DBAL/Types/ArrayType.php create mode 100644 Doctrine/DBAL/Types/BigIntType.php create mode 100644 Doctrine/DBAL/Types/BooleanType.php create mode 100644 Doctrine/DBAL/Types/ConversionException.php create mode 100644 Doctrine/DBAL/Types/DateTimeType.php create mode 100644 Doctrine/DBAL/Types/DateTimeTzType.php create mode 100644 Doctrine/DBAL/Types/DateType.php create mode 100644 Doctrine/DBAL/Types/DecimalType.php create mode 100644 Doctrine/DBAL/Types/FloatType.php create mode 100644 Doctrine/DBAL/Types/IntegerType.php create mode 100644 Doctrine/DBAL/Types/ObjectType.php create mode 100644 Doctrine/DBAL/Types/SmallIntType.php create mode 100644 Doctrine/DBAL/Types/StringType.php create mode 100644 Doctrine/DBAL/Types/TextType.php create mode 100644 Doctrine/DBAL/Types/TimeType.php create mode 100644 Doctrine/DBAL/Types/Type.php create mode 100644 Doctrine/DBAL/Types/VarDateTimeType.php create mode 100644 Doctrine/DBAL/Version.php create mode 100644 Doctrine/ORM/AbstractQuery.php create mode 100644 Doctrine/ORM/Configuration.php create mode 100644 Doctrine/ORM/EntityManager.php create mode 100644 Doctrine/ORM/EntityNotFoundException.php create mode 100644 Doctrine/ORM/EntityRepository.php create mode 100644 Doctrine/ORM/Event/LifecycleEventArgs.php create mode 100644 Doctrine/ORM/Event/LoadClassMetadataEventArgs.php create mode 100644 Doctrine/ORM/Event/OnFlushEventArgs.php create mode 100644 Doctrine/ORM/Event/PreUpdateEventArgs.php create mode 100644 Doctrine/ORM/Events.php create mode 100644 Doctrine/ORM/Id/AbstractIdGenerator.php create mode 100644 Doctrine/ORM/Id/AssignedGenerator.php create mode 100644 Doctrine/ORM/Id/IdentityGenerator.php create mode 100644 Doctrine/ORM/Id/SequenceGenerator.php create mode 100644 Doctrine/ORM/Id/TableGenerator.php create mode 100644 Doctrine/ORM/Internal/CommitOrderCalculator.php create mode 100644 Doctrine/ORM/Internal/Hydration/AbstractHydrator.php create mode 100644 Doctrine/ORM/Internal/Hydration/ArrayHydrator.php create mode 100644 Doctrine/ORM/Internal/Hydration/HydrationException.php create mode 100644 Doctrine/ORM/Internal/Hydration/IterableResult.php create mode 100644 Doctrine/ORM/Internal/Hydration/ObjectHydrator.php create mode 100644 Doctrine/ORM/Internal/Hydration/ScalarHydrator.php create mode 100644 Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php create mode 100644 Doctrine/ORM/Mapping/ClassMetadata.php create mode 100644 Doctrine/ORM/Mapping/ClassMetadataFactory.php create mode 100644 Doctrine/ORM/Mapping/ClassMetadataInfo.php create mode 100644 Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/AnnotationDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/DatabaseDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php create mode 100644 Doctrine/ORM/Mapping/Driver/Driver.php create mode 100644 Doctrine/ORM/Mapping/Driver/DriverChain.php create mode 100644 Doctrine/ORM/Mapping/Driver/PHPDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/StaticPHPDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/XmlDriver.php create mode 100644 Doctrine/ORM/Mapping/Driver/YamlDriver.php create mode 100644 Doctrine/ORM/Mapping/MappingException.php create mode 100644 Doctrine/ORM/NativeQuery.php create mode 100644 Doctrine/ORM/NoResultException.php create mode 100644 Doctrine/ORM/NonUniqueResultException.php create mode 100644 Doctrine/ORM/ORMException.php create mode 100644 Doctrine/ORM/OptimisticLockException.php create mode 100644 Doctrine/ORM/PersistentCollection.php create mode 100644 Doctrine/ORM/Persisters/AbstractCollectionPersister.php create mode 100644 Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php create mode 100644 Doctrine/ORM/Persisters/BasicEntityPersister.php create mode 100644 Doctrine/ORM/Persisters/ElementCollectionPersister.php create mode 100644 Doctrine/ORM/Persisters/JoinedSubclassPersister.php create mode 100644 Doctrine/ORM/Persisters/ManyToManyPersister.php create mode 100644 Doctrine/ORM/Persisters/OneToManyPersister.php create mode 100644 Doctrine/ORM/Persisters/SingleTablePersister.php create mode 100644 Doctrine/ORM/Persisters/UnionSubclassPersister.php create mode 100644 Doctrine/ORM/PessimisticLockException.php create mode 100644 Doctrine/ORM/Proxy/Proxy.php create mode 100644 Doctrine/ORM/Proxy/ProxyException.php create mode 100644 Doctrine/ORM/Proxy/ProxyFactory.php create mode 100644 Doctrine/ORM/Query.php create mode 100644 Doctrine/ORM/Query/AST/ASTException.php create mode 100644 Doctrine/ORM/Query/AST/AggregateExpression.php create mode 100644 Doctrine/ORM/Query/AST/ArithmeticExpression.php create mode 100644 Doctrine/ORM/Query/AST/ArithmeticFactor.php create mode 100644 Doctrine/ORM/Query/AST/ArithmeticTerm.php create mode 100644 Doctrine/ORM/Query/AST/BetweenExpression.php create mode 100644 Doctrine/ORM/Query/AST/CollectionMemberExpression.php create mode 100644 Doctrine/ORM/Query/AST/ComparisonExpression.php create mode 100644 Doctrine/ORM/Query/AST/ConditionalExpression.php create mode 100644 Doctrine/ORM/Query/AST/ConditionalFactor.php create mode 100644 Doctrine/ORM/Query/AST/ConditionalPrimary.php create mode 100644 Doctrine/ORM/Query/AST/ConditionalTerm.php create mode 100644 Doctrine/ORM/Query/AST/DeleteClause.php create mode 100644 Doctrine/ORM/Query/AST/DeleteStatement.php create mode 100644 Doctrine/ORM/Query/AST/EmptyCollectionComparisonExpression.php create mode 100644 Doctrine/ORM/Query/AST/ExistsExpression.php create mode 100644 Doctrine/ORM/Query/AST/FromClause.php create mode 100644 Doctrine/ORM/Query/AST/Functions/AbsFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/ConcatFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/CurrentDateFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/CurrentTimeFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/CurrentTimestampFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/FunctionNode.php create mode 100644 Doctrine/ORM/Query/AST/Functions/LengthFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/LocateFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/LowerFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/ModFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/SizeFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/SqrtFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/SubstringFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/TrimFunction.php create mode 100644 Doctrine/ORM/Query/AST/Functions/UpperFunction.php create mode 100644 Doctrine/ORM/Query/AST/GroupByClause.php create mode 100644 Doctrine/ORM/Query/AST/HavingClause.php create mode 100644 Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php create mode 100644 Doctrine/ORM/Query/AST/InExpression.php create mode 100644 Doctrine/ORM/Query/AST/IndexBy.php create mode 100644 Doctrine/ORM/Query/AST/InputParameter.php create mode 100644 Doctrine/ORM/Query/AST/InstanceOfExpression.php create mode 100644 Doctrine/ORM/Query/AST/Join.php create mode 100644 Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php create mode 100644 Doctrine/ORM/Query/AST/JoinVariableDeclaration.php create mode 100644 Doctrine/ORM/Query/AST/LikeExpression.php create mode 100644 Doctrine/ORM/Query/AST/Literal.php create mode 100644 Doctrine/ORM/Query/AST/Node.php create mode 100644 Doctrine/ORM/Query/AST/NullComparisonExpression.php create mode 100644 Doctrine/ORM/Query/AST/OrderByClause.php create mode 100644 Doctrine/ORM/Query/AST/OrderByItem.php create mode 100644 Doctrine/ORM/Query/AST/PartialObjectExpression.php create mode 100644 Doctrine/ORM/Query/AST/PathExpression.php create mode 100644 Doctrine/ORM/Query/AST/QuantifiedExpression.php create mode 100644 Doctrine/ORM/Query/AST/RangeVariableDeclaration.php create mode 100644 Doctrine/ORM/Query/AST/SelectClause.php create mode 100644 Doctrine/ORM/Query/AST/SelectExpression.php create mode 100644 Doctrine/ORM/Query/AST/SelectStatement.php create mode 100644 Doctrine/ORM/Query/AST/SimpleArithmeticExpression.php create mode 100644 Doctrine/ORM/Query/AST/SimpleSelectClause.php create mode 100644 Doctrine/ORM/Query/AST/SimpleSelectExpression.php create mode 100644 Doctrine/ORM/Query/AST/Subselect.php create mode 100644 Doctrine/ORM/Query/AST/SubselectFromClause.php create mode 100644 Doctrine/ORM/Query/AST/UpdateClause.php create mode 100644 Doctrine/ORM/Query/AST/UpdateItem.php create mode 100644 Doctrine/ORM/Query/AST/UpdateStatement.php create mode 100644 Doctrine/ORM/Query/AST/WhereClause.php create mode 100644 Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php create mode 100644 Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php create mode 100644 Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php create mode 100644 Doctrine/ORM/Query/Exec/SingleSelectExecutor.php create mode 100644 Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php create mode 100644 Doctrine/ORM/Query/Expr.php create mode 100644 Doctrine/ORM/Query/Expr/Andx.php create mode 100644 Doctrine/ORM/Query/Expr/Base.php create mode 100644 Doctrine/ORM/Query/Expr/Comparison.php create mode 100644 Doctrine/ORM/Query/Expr/From.php create mode 100644 Doctrine/ORM/Query/Expr/Func.php create mode 100644 Doctrine/ORM/Query/Expr/GroupBy.php create mode 100644 Doctrine/ORM/Query/Expr/Join.php create mode 100644 Doctrine/ORM/Query/Expr/Literal.php create mode 100644 Doctrine/ORM/Query/Expr/Math.php create mode 100644 Doctrine/ORM/Query/Expr/OrderBy.php create mode 100644 Doctrine/ORM/Query/Expr/Orx.php create mode 100644 Doctrine/ORM/Query/Expr/Select.php create mode 100644 Doctrine/ORM/Query/Lexer.php create mode 100644 Doctrine/ORM/Query/Parser.php create mode 100644 Doctrine/ORM/Query/ParserResult.php create mode 100644 Doctrine/ORM/Query/Printer.php create mode 100644 Doctrine/ORM/Query/QueryException.php create mode 100644 Doctrine/ORM/Query/ResultSetMapping.php create mode 100644 Doctrine/ORM/Query/SqlWalker.php create mode 100644 Doctrine/ORM/Query/TreeWalker.php create mode 100644 Doctrine/ORM/Query/TreeWalkerAdapter.php create mode 100644 Doctrine/ORM/Query/TreeWalkerChain.php create mode 100644 Doctrine/ORM/QueryBuilder.php create mode 100644 Doctrine/ORM/README.markdown create mode 100644 Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/EnsureProductionSettingsCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php create mode 100644 Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php create mode 100644 Doctrine/ORM/Tools/Console/ConsoleRunner.php create mode 100644 Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php create mode 100644 Doctrine/ORM/Tools/Console/MetadataFilter.php create mode 100644 Doctrine/ORM/Tools/ConvertDoctrine1Schema.php create mode 100644 Doctrine/ORM/Tools/DisconnectedClassMetadataFactory.php create mode 100644 Doctrine/ORM/Tools/EntityGenerator.php create mode 100644 Doctrine/ORM/Tools/EntityRepositoryGenerator.php create mode 100644 Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php create mode 100644 Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php create mode 100644 Doctrine/ORM/Tools/Export/ClassMetadataExporter.php create mode 100644 Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php create mode 100644 Doctrine/ORM/Tools/Export/Driver/AnnotationExporter.php create mode 100644 Doctrine/ORM/Tools/Export/Driver/PhpExporter.php create mode 100644 Doctrine/ORM/Tools/Export/Driver/XmlExporter.php create mode 100644 Doctrine/ORM/Tools/Export/Driver/YamlExporter.php create mode 100644 Doctrine/ORM/Tools/Export/ExportException.php create mode 100644 Doctrine/ORM/Tools/SchemaTool.php create mode 100644 Doctrine/ORM/Tools/SchemaValidator.php create mode 100644 Doctrine/ORM/Tools/ToolEvents.php create mode 100644 Doctrine/ORM/Tools/ToolsException.php create mode 100644 Doctrine/ORM/TransactionRequiredException.php create mode 100644 Doctrine/ORM/UnitOfWork.php create mode 100644 Doctrine/ORM/Version.php create mode 100644 Doctrine/Symfony/Component/Console/Application.php create mode 100644 Doctrine/Symfony/Component/Console/Command/Command.php create mode 100644 Doctrine/Symfony/Component/Console/Command/HelpCommand.php create mode 100644 Doctrine/Symfony/Component/Console/Command/ListCommand.php create mode 100644 Doctrine/Symfony/Component/Console/Helper/DialogHelper.php create mode 100644 Doctrine/Symfony/Component/Console/Helper/FormatterHelper.php create mode 100644 Doctrine/Symfony/Component/Console/Helper/Helper.php create mode 100644 Doctrine/Symfony/Component/Console/Helper/HelperInterface.php create mode 100644 Doctrine/Symfony/Component/Console/Helper/HelperSet.php create mode 100644 Doctrine/Symfony/Component/Console/Input/ArgvInput.php create mode 100644 Doctrine/Symfony/Component/Console/Input/ArrayInput.php create mode 100644 Doctrine/Symfony/Component/Console/Input/Input.php create mode 100644 Doctrine/Symfony/Component/Console/Input/InputArgument.php create mode 100644 Doctrine/Symfony/Component/Console/Input/InputDefinition.php create mode 100644 Doctrine/Symfony/Component/Console/Input/InputInterface.php create mode 100644 Doctrine/Symfony/Component/Console/Input/InputOption.php create mode 100644 Doctrine/Symfony/Component/Console/Input/StringInput.php create mode 100644 Doctrine/Symfony/Component/Console/Output/ConsoleOutput.php create mode 100644 Doctrine/Symfony/Component/Console/Output/NullOutput.php create mode 100644 Doctrine/Symfony/Component/Console/Output/Output.php create mode 100644 Doctrine/Symfony/Component/Console/Output/OutputInterface.php create mode 100644 Doctrine/Symfony/Component/Console/Output/StreamOutput.php create mode 100644 Doctrine/Symfony/Component/Console/Shell.php create mode 100644 Doctrine/Symfony/Component/Console/Tester/ApplicationTester.php create mode 100644 Doctrine/Symfony/Component/Console/Tester/CommandTester.php create mode 100644 Doctrine/Symfony/Component/Yaml/Dumper.php create mode 100644 Doctrine/Symfony/Component/Yaml/Exception.php create mode 100644 Doctrine/Symfony/Component/Yaml/Inline.php create mode 100644 Doctrine/Symfony/Component/Yaml/Parser.php create mode 100644 Doctrine/Symfony/Component/Yaml/ParserException.php create mode 100644 Doctrine/Symfony/Component/Yaml/Yaml.php create mode 100644 Proxies/ScoreProxy.php create mode 100644 Proxies/SessionProxy.php create mode 100644 Proxies/UserProxy.php create mode 100644 Tim/Score.php create mode 100644 Tim/Session.php create mode 100644 Tim/User.php create mode 100644 chatserver.php create mode 100644 config/mappings/Score.dcm.xml create mode 100644 config/mappings/Session.dcm.xml create mode 100644 config/mappings/User.dcm.xml create mode 100644 doctrine-orm/LICENSE create mode 100644 doctrine-orm/bin/doctrine create mode 100644 doctrine-orm/bin/doctrine.bat create mode 100644 doctrine-orm/bin/doctrine.php create mode 100644 doctrine.php create mode 100644 index.php create mode 100644 init.php diff --git a/Doctrine/Common/Annotations/Annotation.php b/Doctrine/Common/Annotations/Annotation.php new file mode 100644 index 0000000..e2bf42b --- /dev/null +++ b/Doctrine/Common/Annotations/Annotation.php @@ -0,0 +1,81 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Annotations class + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Annotation +{ + /** + * Value property. Common among all derived classes. + * + * @var string + */ + public $value; + + /** + * Constructor + * + * @param array $data Key-value for properties to be defined in this class + */ + public final function __construct(array $data) + { + foreach ($data as $key => $value) { + $this->$key = $value; + } + } + + /** + * Error handler for unknown property accessor in Annotation class. + * + * @param string $name Unknown property name + */ + public function __get($name) + { + throw new \BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + ); + } + + /** + * Error handler for unknown property mutator in Annotation class. + * + * @param string $name Unkown property name + * @param mixed $value Property value + */ + public function __set($name, $value) + { + throw new \BadMethodCallException( + sprintf("Unknown property '%s' on annotation '%s'.", $name, get_class($this)) + ); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Annotations/AnnotationException.php b/Doctrine/Common/Annotations/AnnotationException.php new file mode 100644 index 0000000..d2c6cce --- /dev/null +++ b/Doctrine/Common/Annotations/AnnotationException.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Description of AnnotationException + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AnnotationException extends \Exception +{ + /** + * Creates a new AnnotationException describing a Syntax error. + * + * @param string $message Exception message + * @return AnnotationException + */ + public static function syntaxError($message) + { + return new self('[Syntax Error] ' . $message); + } + + /** + * Creates a new AnnotationException describing a Semantical error. + * + * @param string $message Exception message + * @return AnnotationException + */ + public static function semanticalError($message) + { + return new self('[Semantical Error] ' . $message); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Annotations/AnnotationReader.php b/Doctrine/Common/Annotations/AnnotationReader.php new file mode 100644 index 0000000..8fd8577 --- /dev/null +++ b/Doctrine/Common/Annotations/AnnotationReader.php @@ -0,0 +1,248 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Closure, + ReflectionClass, + ReflectionMethod, + ReflectionProperty, + Doctrine\Common\Cache\Cache; + +/** + * A reader for docblock annotations. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AnnotationReader +{ + /** + * Cache salt + * + * @var string + * @static + */ + private static $CACHE_SALT = '@'; + + /** + * Annotations Parser + * + * @var Doctrine\Common\Annotations\Parser + */ + private $parser; + + /** + * Cache mechanism to store processed Annotations + * + * @var Doctrine\Common\Cache\Cache + */ + private $cache; + + /** + * Constructor. Initializes a new AnnotationReader that uses the given Cache provider. + * + * @param Cache $cache The cache provider to use. If none is provided, ArrayCache is used. + * @param Parser $parser The parser to use. If none is provided, the default parser is used. + */ + public function __construct(Cache $cache = null, Parser $parser = null) + { + $this->parser = $parser ?: new Parser; + $this->cache = $cache ?: new \Doctrine\Common\Cache\ArrayCache; + } + + /** + * Sets the default namespace that the AnnotationReader should assume for annotations + * with not fully qualified names. + * + * @param string $defaultNamespace + */ + public function setDefaultAnnotationNamespace($defaultNamespace) + { + $this->parser->setDefaultAnnotationNamespace($defaultNamespace); + } + + /** + * Sets the custom function to use for creating new annotations on the + * underlying parser. + * + * The function is supplied two arguments. The first argument is the name + * of the annotation and the second argument an array of values for this + * annotation. The function is assumed to return an object or NULL. + * Whenever the function returns NULL for an annotation, the implementation falls + * back to the default annotation creation process of the underlying parser. + * + * @param Closure $func + */ + public function setAnnotationCreationFunction(Closure $func) + { + $this->parser->setAnnotationCreationFunction($func); + } + + /** + * Sets an alias for an annotation namespace. + * + * @param $namespace + * @param $alias + */ + public function setAnnotationNamespaceAlias($namespace, $alias) + { + $this->parser->setAnnotationNamespaceAlias($namespace, $alias); + } + + /** + * Sets a flag whether to try to autoload annotation classes, as well as to distinguish + * between what is an annotation and what not by triggering autoloading. + * + * NOTE: Autoloading of annotation classes is inefficient and requires silently failing + * autoloaders. In particular, setting this option to TRUE renders this AnnotationReader + * incompatible with a {@link ClassLoader}. + * @param boolean $bool Boolean flag. + */ + public function setAutoloadAnnotations($bool) + { + $this->parser->setAutoloadAnnotations($bool); + } + + /** + * Gets a flag whether to try to autoload annotation classes. + * + * @see setAutoloadAnnotations + * @return boolean + */ + public function getAutoloadAnnotations() + { + return $this->parser->getAutoloadAnnotations(); + } + + /** + * Gets the annotations applied to a class. + * + * @param string|ReflectionClass $class The name or ReflectionClass of the class from which + * the class annotations should be read. + * @return array An array of Annotations. + */ + public function getClassAnnotations(ReflectionClass $class) + { + $cacheKey = $class->getName() . self::$CACHE_SALT; + + // Attempt to grab data from cache + if (($data = $this->cache->fetch($cacheKey)) !== false) { + return $data; + } + + $annotations = $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); + $this->cache->save($cacheKey, $annotations, null); + + return $annotations; + } + + /** + * Gets a class annotation. + * + * @param $class + * @param string $annotation The name of the annotation. + * @return The Annotation or NULL, if the requested annotation does not exist. + */ + public function getClassAnnotation(ReflectionClass $class, $annotation) + { + $annotations = $this->getClassAnnotations($class); + + return isset($annotations[$annotation]) ? $annotations[$annotation] : null; + } + + /** + * Gets the annotations applied to a property. + * + * @param string|ReflectionClass $class The name or ReflectionClass of the class that owns the property. + * @param string|ReflectionProperty $property The name or ReflectionProperty of the property + * from which the annotations should be read. + * @return array An array of Annotations. + */ + public function getPropertyAnnotations(ReflectionProperty $property) + { + $cacheKey = $property->getDeclaringClass()->getName() . '$' . $property->getName() . self::$CACHE_SALT; + + // Attempt to grab data from cache + if (($data = $this->cache->fetch($cacheKey)) !== false) { + return $data; + } + + $context = 'property ' . $property->getDeclaringClass()->getName() . "::\$" . $property->getName(); + $annotations = $this->parser->parse($property->getDocComment(), $context); + $this->cache->save($cacheKey, $annotations, null); + + return $annotations; + } + + /** + * Gets a property annotation. + * + * @param ReflectionProperty $property + * @param string $annotation The name of the annotation. + * @return The Annotation or NULL, if the requested annotation does not exist. + */ + public function getPropertyAnnotation(ReflectionProperty $property, $annotation) + { + $annotations = $this->getPropertyAnnotations($property); + + return isset($annotations[$annotation]) ? $annotations[$annotation] : null; + } + + /** + * Gets the annotations applied to a method. + * + * @param string|ReflectionClass $class The name or ReflectionClass of the class that owns the method. + * @param string|ReflectionMethod $property The name or ReflectionMethod of the method from which + * the annotations should be read. + * @return array An array of Annotations. + */ + public function getMethodAnnotations(ReflectionMethod $method) + { + $cacheKey = $method->getDeclaringClass()->getName() . '#' . $method->getName() . self::$CACHE_SALT; + + // Attempt to grab data from cache + if (($data = $this->cache->fetch($cacheKey)) !== false) { + return $data; + } + + $context = 'method ' . $method->getDeclaringClass()->getName() . '::' . $method->getName() . '()'; + $annotations = $this->parser->parse($method->getDocComment(), $context); + $this->cache->save($cacheKey, $annotations, null); + + return $annotations; + } + + /** + * Gets a method annotation. + * + * @param ReflectionMethod $method + * @param string $annotation The name of the annotation. + * @return The Annotation or NULL, if the requested annotation does not exist. + */ + public function getMethodAnnotation(ReflectionMethod $method, $annotation) + { + $annotations = $this->getMethodAnnotations($method); + + return isset($annotations[$annotation]) ? $annotations[$annotation] : null; + } +} \ No newline at end of file diff --git a/Doctrine/Common/Annotations/Lexer.php b/Doctrine/Common/Annotations/Lexer.php new file mode 100644 index 0000000..72e4fb9 --- /dev/null +++ b/Doctrine/Common/Annotations/Lexer.php @@ -0,0 +1,157 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +/** + * Simple lexer for docblock annotations. + * + * This Lexer can be subclassed to customize certain aspects of the annotation + * lexing (token recognition) process. Note though that currently no special care + * is taken to maintain full backwards compatibility for subclasses. Implementation + * details of the default Lexer can change without explicit notice. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Lexer extends \Doctrine\Common\Lexer +{ + const T_NONE = 1; + const T_IDENTIFIER = 2; + const T_INTEGER = 3; + const T_STRING = 4; + const T_FLOAT = 5; + + const T_AT = 101; + const T_CLOSE_CURLY_BRACES = 102; + const T_CLOSE_PARENTHESIS = 103; + const T_COMMA = 104; + const T_EQUALS = 105; + const T_FALSE = 106; + const T_NAMESPACE_SEPARATOR = 107; + const T_OPEN_CURLY_BRACES = 108; + const T_OPEN_PARENTHESIS = 109; + const T_TRUE = 110; + + /** + * @inheritdoc + */ + protected function getCatchablePatterns() + { + return array( + '[a-z_][a-z0-9_:]*', + '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', + '"(?:[^"]|"")*"' + ); + } + + /** + * @inheritdoc + */ + protected function getNonCatchablePatterns() + { + return array('\s+', '\*+', '(.)'); + } + + /** + * @inheritdoc + */ + protected function getType(&$value) + { + $type = self::T_NONE; + $newVal = $this->getNumeric($value); + + // Checking numeric value + if ($newVal !== false) { + $value = $newVal; + + return (strpos($value, '.') !== false || stripos($value, 'e') !== false) + ? self::T_FLOAT : self::T_INTEGER; + } + + if ($value[0] === '"') { + $value = str_replace('""', '"', substr($value, 1, strlen($value) - 2)); + + return self::T_STRING; + } else { + switch (strtolower($value)) { + case '@': + return self::T_AT; + + case ',': + return self::T_COMMA; + + case '(': + return self::T_OPEN_PARENTHESIS; + + case ')': + return self::T_CLOSE_PARENTHESIS; + + case '{': + return self::T_OPEN_CURLY_BRACES; + + case '}': return self::T_CLOSE_CURLY_BRACES; + case '=': + return self::T_EQUALS; + + case '\\': + return self::T_NAMESPACE_SEPARATOR; + + case 'true': + return self::T_TRUE; + + case 'false': + return self::T_FALSE; + + default: + if (ctype_alpha($value[0]) || $value[0] === '_') { + return self::T_IDENTIFIER; + } + + break; + } + } + + return $type; + } + + /** + * Checks if a value is numeric or not + * + * @param mixed $value Value to be inspected + * @return boolean|integer|float Processed value + * @todo Inline + */ + private function getNumeric($value) + { + if ( ! is_scalar($value)) { + return false; + } + + // Checking for valid numeric numbers: 1.234, -1.234e-2 + if (is_numeric($value)) { + return $value; + } + + return false; + } +} \ No newline at end of file diff --git a/Doctrine/Common/Annotations/Parser.php b/Doctrine/Common/Annotations/Parser.php new file mode 100644 index 0000000..5a965d9 --- /dev/null +++ b/Doctrine/Common/Annotations/Parser.php @@ -0,0 +1,545 @@ +. + */ + +namespace Doctrine\Common\Annotations; + +use Closure, Doctrine\Common\ClassLoader; + +/** + * A simple parser for docblock annotations. + * + * This Parser can be subclassed to customize certain aspects of the annotation + * parsing and/or creation process. Note though that currently no special care + * is taken to maintain full backwards compatibility for subclasses. Implementation + * details of the default Parser can change without explicit notice. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Parser +{ + /** + * Some common tags that are stripped prior to parsing in order to reduce parsing overhead. + * + * @var array + */ + private static $strippedTags = array( + "{@internal", "{@inheritdoc", "{@link" + ); + + /** + * The lexer. + * + * @var Doctrine\Common\Annotations\Lexer + */ + private $lexer; + + /** + * Flag to control if the current annotation is nested or not. + * + * @var boolean + */ + protected $isNestedAnnotation = false; + + /** + * Default namespace for annotations. + * + * @var string + */ + private $defaultAnnotationNamespace = ''; + + /** + * Hashmap to store namespace aliases. + * + * @var array + */ + private $namespaceAliases = array(); + + /** + * @var string + */ + private $context = ''; + + /** + * @var boolean Whether to try to autoload annotations that are not yet defined. + */ + private $autoloadAnnotations = false; + + /** + * @var Closure The custom function used to create new annotations, if any. + */ + private $annotationCreationFunction; + + /** + * Constructs a new AnnotationParser. + */ + public function __construct(Lexer $lexer = null) + { + $this->lexer = $lexer ?: new Lexer; + } + + /** + * Gets the lexer used by this parser. + * + * @return Lexer The lexer. + */ + public function getLexer() + { + return $this->lexer; + } + + /** + * Sets a flag whether to try to autoload annotation classes, as well as to distinguish + * between what is an annotation and what not by triggering autoloading. + * + * NOTE: Autoloading of annotation classes is inefficient and requires silently failing + * autoloaders. In particular, setting this option to TRUE renders the Parser + * incompatible with a {@link ClassLoader}. + * @param boolean $bool Boolean flag. + */ + public function setAutoloadAnnotations($bool) + { + $this->autoloadAnnotations = $bool; + } + + /** + * Sets the custom function to use for creating new annotations. + * + * The function is supplied two arguments. The first argument is the name + * of the annotation and the second argument an array of values for this + * annotation. The function is assumed to return an object or NULL. + * Whenever the function returns NULL for an annotation, the parser falls + * back to the default annotation creation process. + * + * Whenever the function returns NULL for an annotation, the implementation falls + * back to the default annotation creation process. + * + * @param Closure $func + */ + public function setAnnotationCreationFunction(Closure $func) + { + $this->annotationCreationFunction = $func; + } + + /** + * Gets a flag whether to try to autoload annotation classes. + * + * @see setAutoloadAnnotations + * @return boolean + */ + public function getAutoloadAnnotations() + { + return $this->autoloadAnnotations; + } + + /** + * Sets the default namespace that is assumed for an annotation that does not + * define a namespace prefix. + * + * @param string $defaultNamespace + */ + public function setDefaultAnnotationNamespace($defaultNamespace) + { + $this->defaultAnnotationNamespace = $defaultNamespace; + } + + /** + * Sets an alias for an annotation namespace. + * + * @param string $namespace + * @param string $alias + */ + public function setAnnotationNamespaceAlias($namespace, $alias) + { + $this->namespaceAliases[$alias] = $namespace; + } + + /** + * Gets the namespace alias mappings used by this parser. + * + * @return array The namespace alias mappings. + */ + public function getNamespaceAliases() + { + return $this->namespaceAliases; + } + + /** + * Parses the given docblock string for annotations. + * + * @param string $docBlockString The docblock string to parse. + * @param string $context The parsing context. + * @return array Array of annotations. If no annotations are found, an empty array is returned. + */ + public function parse($docBlockString, $context='') + { + $this->context = $context; + + // Strip out some known inline tags. + $input = str_replace(self::$strippedTags, '', $docBlockString); + + // Cut of the beginning of the input until the first '@'. + $input = substr($input, strpos($input, '@')); + + $this->lexer->reset(); + $this->lexer->setInput(trim($input, '* /')); + $this->lexer->moveNext(); + + if ($this->lexer->isNextToken(Lexer::T_AT)) { + return $this->Annotations(); + } + + return array(); + } + + /** + * Attempts to match the given token with the current lookahead token. + * If they match, updates the lookahead token; otherwise raises a syntax error. + * + * @param int Token type. + * @return bool True if tokens match; false otherwise. + */ + public function match($token) + { + if ( ! ($this->lexer->lookahead['type'] === $token)) { + $this->syntaxError($this->lexer->getLiteral($token)); + } + $this->lexer->moveNext(); + } + + /** + * Generates a new syntax error. + * + * @param string $expected Expected string. + * @param array $token Optional token. + * @throws AnnotationException + */ + private function syntaxError($expected, $token = null) + { + if ($token === null) { + $token = $this->lexer->lookahead; + } + + $message = "Expected {$expected}, got "; + + if ($this->lexer->lookahead === null) { + $message .= 'end of string'; + } else { + $message .= "'{$token['value']}' at position {$token['position']}"; + } + + if (strlen($this->context)) { + $message .= ' in ' . $this->context; + } + + $message .= '.'; + + throw AnnotationException::syntaxError($message); + } + + /** + * Annotations ::= Annotation {[ "*" ]* [Annotation]}* + * + * @return array + */ + public function Annotations() + { + $this->isNestedAnnotation = false; + + $annotations = array(); + $annot = $this->Annotation(); + + if ($annot !== false) { + $annotations[get_class($annot)] = $annot; + $this->lexer->skipUntil(Lexer::T_AT); + } + + while ($this->lexer->lookahead !== null && $this->lexer->isNextToken(Lexer::T_AT)) { + $this->isNestedAnnotation = false; + $annot = $this->Annotation(); + + if ($annot !== false) { + $annotations[get_class($annot)] = $annot; + $this->lexer->skipUntil(Lexer::T_AT); + } + } + + return $annotations; + } + + /** + * Annotation ::= "@" AnnotationName ["(" [Values] ")"] + * AnnotationName ::= QualifiedName | SimpleName | AliasedName + * QualifiedName ::= NameSpacePart "\" {NameSpacePart "\"}* SimpleName + * AliasedName ::= Alias ":" SimpleName + * NameSpacePart ::= identifier + * SimpleName ::= identifier + * Alias ::= identifier + * + * @return mixed False if it is not a valid annotation. + */ + public function Annotation() + { + $values = array(); + $nameParts = array(); + + $this->match(Lexer::T_AT); + $this->match(Lexer::T_IDENTIFIER); + $nameParts[] = $this->lexer->token['value']; + + while ($this->lexer->isNextToken(Lexer::T_NAMESPACE_SEPARATOR)) { + $this->match(Lexer::T_NAMESPACE_SEPARATOR); + $this->match(Lexer::T_IDENTIFIER); + $nameParts[] = $this->lexer->token['value']; + } + + // Effectively pick the name of the class (append default NS if none, grab from NS alias, etc) + if (strpos($nameParts[0], ':')) { + list ($alias, $nameParts[0]) = explode(':', $nameParts[0]); + + // If the namespace alias doesnt exist, skip until next annotation + if ( ! isset($this->namespaceAliases[$alias])) { + $this->lexer->skipUntil(Lexer::T_AT); + return false; + } + + $name = $this->namespaceAliases[$alias] . implode('\\', $nameParts); + } else if (count($nameParts) == 1) { + $name = $this->defaultAnnotationNamespace . $nameParts[0]; + } else { + $name = implode('\\', $nameParts); + } + + // Does the annotation class exist? + if ( ! class_exists($name, $this->autoloadAnnotations)) { + $this->lexer->skipUntil(Lexer::T_AT); + return false; + } + + // Next will be nested + $this->isNestedAnnotation = true; + + if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { + $this->match(Lexer::T_OPEN_PARENTHESIS); + + if ( ! $this->lexer->isNextToken(Lexer::T_CLOSE_PARENTHESIS)) { + $values = $this->Values(); + } + + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } + + if ($this->annotationCreationFunction !== null) { + $func = $this->annotationCreationFunction; + $annot = $func($name, $values); + } + + return isset($annot) ? $annot : $this->newAnnotation($name, $values); + } + + /** + * Values ::= Array | Value {"," Value}* + * + * @return array + */ + public function Values() + { + $values = array(); + + // Handle the case of a single array as value, i.e. @Foo({....}) + if ($this->lexer->isNextToken(Lexer::T_OPEN_CURLY_BRACES)) { + $values['value'] = $this->Value(); + return $values; + } + + $values[] = $this->Value(); + + while ($this->lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $value = $this->Value(); + + if ( ! is_array($value)) { + $this->syntaxError('Value', $value); + } + + $values[] = $value; + } + + foreach ($values as $k => $value) { + if (is_array($value) && is_string(key($value))) { + $key = key($value); + $values[$key] = $value[$key]; + } else { + $values['value'] = $value; + } + + unset($values[$k]); + } + + return $values; + } + + /** + * Value ::= PlainValue | FieldAssignment + * + * @return mixed + */ + public function Value() + { + $peek = $this->lexer->glimpse(); + + if ($peek['value'] === '=') { + return $this->FieldAssignment(); + } + + return $this->PlainValue(); + } + + /** + * PlainValue ::= integer | string | float | boolean | Array | Annotation + * + * @return mixed + */ + public function PlainValue() + { + if ($this->lexer->isNextToken(Lexer::T_OPEN_CURLY_BRACES)) { + return $this->Arrayx(); + } + + if ($this->lexer->isNextToken(Lexer::T_AT)) { + return $this->Annotation(); + } + + switch ($this->lexer->lookahead['type']) { + case Lexer::T_STRING: + $this->match(Lexer::T_STRING); + return $this->lexer->token['value']; + + case Lexer::T_INTEGER: + $this->match(Lexer::T_INTEGER); + return $this->lexer->token['value']; + + case Lexer::T_FLOAT: + $this->match(Lexer::T_FLOAT); + return $this->lexer->token['value']; + + case Lexer::T_TRUE: + $this->match(Lexer::T_TRUE); + return true; + + case Lexer::T_FALSE: + $this->match(Lexer::T_FALSE); + return false; + + default: + $this->syntaxError('PlainValue'); + } + } + + /** + * FieldAssignment ::= FieldName "=" PlainValue + * FieldName ::= identifier + * + * @return array + */ + public function FieldAssignment() + { + $this->match(Lexer::T_IDENTIFIER); + $fieldName = $this->lexer->token['value']; + $this->match(Lexer::T_EQUALS); + + return array($fieldName => $this->PlainValue()); + } + + /** + * Array ::= "{" ArrayEntry {"," ArrayEntry}* "}" + * + * @return array + */ + public function Arrayx() + { + $array = $values = array(); + + $this->match(Lexer::T_OPEN_CURLY_BRACES); + $values[] = $this->ArrayEntry(); + + while ($this->lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $values[] = $this->ArrayEntry(); + } + + $this->match(Lexer::T_CLOSE_CURLY_BRACES); + + foreach ($values as $value) { + list ($key, $val) = $value; + + if ($key !== null) { + $array[$key] = $val; + } else { + $array[] = $val; + } + } + + return $array; + } + + /** + * ArrayEntry ::= Value | KeyValuePair + * KeyValuePair ::= Key "=" PlainValue + * Key ::= string | integer + * + * @return array + */ + public function ArrayEntry() + { + $peek = $this->lexer->glimpse(); + + if ($peek['value'] == '=') { + $this->match( + $this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_STRING + ); + + $key = $this->lexer->token['value']; + $this->match(Lexer::T_EQUALS); + + return array($key, $this->PlainValue()); + } + + return array(null, $this->Value()); + } + + /** + * Constructs a new annotation with a given map of values. + * + * The default construction procedure is to instantiate a new object of a class + * with the same name as the annotation. Subclasses can override this method to + * change the construction process of new annotations. + * + * @param string The name of the annotation. + * @param array The map of annotation values. + * @return mixed The new annotation with the given values. + */ + protected function newAnnotation($name, array $values) + { + return new $name($values); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/AbstractCache.php b/Doctrine/Common/Cache/AbstractCache.php new file mode 100644 index 0000000..9bba8f6 --- /dev/null +++ b/Doctrine/Common/Cache/AbstractCache.php @@ -0,0 +1,226 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Base class for cache driver implementations. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +abstract class AbstractCache implements Cache +{ + /** @var string The cache id to store the index of cache ids under */ + private $_cacheIdsIndexId = 'doctrine_cache_ids'; + + /** @var string The namespace to prefix all cache ids with */ + private $_namespace = null; + + /** + * Set the namespace to prefix all cache ids with. + * + * @param string $namespace + * @return void + */ + public function setNamespace($namespace) + { + $this->_namespace = $namespace; + } + + /** + * {@inheritdoc} + */ + public function fetch($id) + { + return $this->_doFetch($this->_getNamespacedId($id)); + } + + /** + * {@inheritdoc} + */ + public function contains($id) + { + return $this->_doContains($this->_getNamespacedId($id)); + } + + /** + * {@inheritdoc} + */ + public function save($id, $data, $lifeTime = 0) + { + return $this->_doSave($this->_getNamespacedId($id), $data, $lifeTime); + } + + /** + * {@inheritdoc} + */ + public function delete($id) + { + $id = $this->_getNamespacedId($id); + + if (strpos($id, '*') !== false) { + return $this->deleteByRegex('/' . str_replace('*', '.*', $id) . '/'); + } + + return $this->_doDelete($id); + } + + /** + * Delete all cache entries. + * + * @return array $deleted Array of the deleted cache ids + */ + public function deleteAll() + { + $ids = $this->getIds(); + + foreach ($ids as $id) { + $this->delete($id); + } + + return $ids; + } + + /** + * Delete cache entries where the id matches a PHP regular expressions + * + * @param string $regex + * @return array $deleted Array of the deleted cache ids + */ + public function deleteByRegex($regex) + { + $deleted = array(); + + $ids = $this->getIds(); + + foreach ($ids as $id) { + if (preg_match($regex, $id)) { + $this->delete($id); + $deleted[] = $id; + } + } + + return $deleted; + } + + /** + * Delete cache entries where the id has the passed prefix + * + * @param string $prefix + * @return array $deleted Array of the deleted cache ids + */ + public function deleteByPrefix($prefix) + { + $deleted = array(); + + $prefix = $this->_getNamespacedId($prefix); + $ids = $this->getIds(); + + foreach ($ids as $id) { + if (strpos($id, $prefix) === 0) { + $this->delete($id); + $deleted[] = $id; + } + } + + return $deleted; + } + + /** + * Delete cache entries where the id has the passed suffix + * + * @param string $suffix + * @return array $deleted Array of the deleted cache ids + */ + public function deleteBySuffix($suffix) + { + $deleted = array(); + + $ids = $this->getIds(); + + foreach ($ids as $id) { + if (substr($id, -1 * strlen($suffix)) === $suffix) { + $this->delete($id); + $deleted[] = $id; + } + } + + return $deleted; + } + + /** + * Prefix the passed id with the configured namespace value + * + * @param string $id The id to namespace + * @return string $id The namespaced id + */ + private function _getNamespacedId($id) + { + if ( ! $this->_namespace || strpos($id, $this->_namespace) === 0) { + return $id; + } else { + return $this->_namespace . $id; + } + } + + /** + * Fetches an entry from the cache. + * + * @param string $id cache id The id of the cache entry to fetch. + * @return string The cached data or FALSE, if no cache entry exists for the given id. + */ + abstract protected function _doFetch($id); + + /** + * Test if an entry exists in the cache. + * + * @param string $id cache id The cache id of the entry to check for. + * @return boolean TRUE if a cache entry exists for the given cache id, FALSE otherwise. + */ + abstract protected function _doContains($id); + + /** + * Puts data into the cache. + * + * @param string $id The cache id. + * @param string $data The cache entry/data. + * @param int $lifeTime The lifetime. If != false, sets a specific lifetime for this cache entry (null => infinite lifeTime). + * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. + */ + abstract protected function _doSave($id, $data, $lifeTime = false); + + /** + * Deletes a cache entry. + * + * @param string $id cache id + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + abstract protected function _doDelete($id); + + /** + * Get an array of all the cache ids stored + * + * @return array $ids + */ + abstract public function getIds(); +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/ApcCache.php b/Doctrine/Common/Cache/ApcCache.php new file mode 100644 index 0000000..f3a9a95 --- /dev/null +++ b/Doctrine/Common/Cache/ApcCache.php @@ -0,0 +1,90 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * APC cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + * @todo Rename: APCCache + */ +class ApcCache extends AbstractCache +{ + /** + * {@inheritdoc} + */ + public function getIds() + { + $ci = apc_cache_info('user'); + $keys = array(); + + foreach ($ci['cache_list'] as $entry) { + $keys[] = $entry['info']; + } + + return $keys; + } + + /** + * {@inheritdoc} + */ + protected function _doFetch($id) + { + return apc_fetch($id); + } + + /** + * {@inheritdoc} + */ + protected function _doContains($id) + { + $found = false; + + apc_fetch($id, $found); + + return $found; + } + + /** + * {@inheritdoc} + */ + protected function _doSave($id, $data, $lifeTime = 0) + { + return (bool) apc_store($id, $data, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function _doDelete($id) + { + return apc_delete($id); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/ArrayCache.php b/Doctrine/Common/Cache/ArrayCache.php new file mode 100644 index 0000000..ada233b --- /dev/null +++ b/Doctrine/Common/Cache/ArrayCache.php @@ -0,0 +1,91 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Array cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class ArrayCache extends AbstractCache +{ + /** + * @var array $data + */ + private $data = array(); + + /** + * {@inheritdoc} + */ + public function getIds() + { + return array_keys($this->data); + } + + /** + * {@inheritdoc} + */ + protected function _doFetch($id) + { + if (isset($this->data[$id])) { + return $this->data[$id]; + } + + return false; + } + + /** + * {@inheritdoc} + */ + protected function _doContains($id) + { + return isset($this->data[$id]); + } + + /** + * {@inheritdoc} + */ + protected function _doSave($id, $data, $lifeTime = 0) + { + $this->data[$id] = $data; + + return true; + } + + /** + * {@inheritdoc} + */ + protected function _doDelete($id) + { + unset($this->data[$id]); + + return true; + } +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/Cache.php b/Doctrine/Common/Cache/Cache.php new file mode 100644 index 0000000..e4cb1c0 --- /dev/null +++ b/Doctrine/Common/Cache/Cache.php @@ -0,0 +1,71 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Interface for cache drivers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface Cache +{ + /** + * Fetches an entry from the cache. + * + * @param string $id cache id The id of the cache entry to fetch. + * @return string The cached data or FALSE, if no cache entry exists for the given id. + */ + function fetch($id); + + /** + * Test if an entry exists in the cache. + * + * @param string $id cache id The cache id of the entry to check for. + * @return boolean TRUE if a cache entry exists for the given cache id, FALSE otherwise. + */ + function contains($id); + + /** + * Puts data into the cache. + * + * @param string $id The cache id. + * @param string $data The cache entry/data. + * @param int $lifeTime The lifetime. If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime). + * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise. + */ + function save($id, $data, $lifeTime = 0); + + /** + * Deletes a cache entry. + * + * @param string $id cache id + * @return boolean TRUE if the cache entry was successfully deleted, FALSE otherwise. + */ + function delete($id); +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/MemcacheCache.php b/Doctrine/Common/Cache/MemcacheCache.php new file mode 100644 index 0000000..a76bd17 --- /dev/null +++ b/Doctrine/Common/Cache/MemcacheCache.php @@ -0,0 +1,123 @@ +. + */ + +namespace Doctrine\Common\Cache; + +use \Memcache; + +/** + * Memcache cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class MemcacheCache extends AbstractCache +{ + /** + * @var Memcache + */ + private $_memcache; + + /** + * Sets the memcache instance to use. + * + * @param Memcache $memcache + */ + public function setMemcache(Memcache $memcache) + { + $this->_memcache = $memcache; + } + + /** + * Gets the memcache instance used by the cache. + * + * @return Memcache + */ + public function getMemcache() + { + return $this->_memcache; + } + + /** + * {@inheritdoc} + */ + public function getIds() + { + $keys = array(); + $allSlabs = $this->_memcache->getExtendedStats('slabs'); + + foreach ($allSlabs as $server => $slabs) { + if (is_array($slabs)) { + foreach (array_keys($slabs) as $slabId) { + $dump = $this->_memcache->getExtendedStats('cachedump', (int) $slabId); + + if ($dump) { + foreach ($dump as $entries) { + if ($entries) { + $keys = array_merge($keys, array_keys($entries)); + } + } + } + } + } + } + return $keys; + } + + /** + * {@inheritdoc} + */ + protected function _doFetch($id) + { + return $this->_memcache->get($id); + } + + /** + * {@inheritdoc} + */ + protected function _doContains($id) + { + return (bool) $this->_memcache->get($id); + } + + /** + * {@inheritdoc} + */ + protected function _doSave($id, $data, $lifeTime = 0) + { + return $this->_memcache->set($id, $data, 0, (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function _doDelete($id) + { + return $this->_memcache->delete($id); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Cache/XcacheCache.php b/Doctrine/Common/Cache/XcacheCache.php new file mode 100644 index 0000000..d180730 --- /dev/null +++ b/Doctrine/Common/Cache/XcacheCache.php @@ -0,0 +1,105 @@ +. + */ + +namespace Doctrine\Common\Cache; + +/** + * Xcache cache driver. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author David Abdemoulaie + */ +class XcacheCache extends AbstractCache +{ + /** + * {@inheritdoc} + */ + public function getIds() + { + $this->_checkAuth(); + $keys = array(); + + for ($i = 0, $count = xcache_count(XC_TYPE_VAR); $i < $count; $i++) { + $entries = xcache_list(XC_TYPE_VAR, $i); + + if (is_array($entries['cache_list'])) { + foreach ($entries['cache_list'] as $entry) { + $keys[] = $entry['name']; + } + } + } + + return $keys; + } + + /** + * {@inheritdoc} + */ + protected function _doFetch($id) + { + return $this->_doContains($id) ? unserialize(xcache_get($id)) : false; + } + + /** + * {@inheritdoc} + */ + protected function _doContains($id) + { + return xcache_isset($id); + } + + /** + * {@inheritdoc} + */ + protected function _doSave($id, $data, $lifeTime = 0) + { + return xcache_set($id, serialize($data), (int) $lifeTime); + } + + /** + * {@inheritdoc} + */ + protected function _doDelete($id) + { + return xcache_unset($id); + } + + + /** + * Checks that xcache.admin.enable_auth is Off + * + * @throws \BadMethodCallException When xcache.admin.enable_auth is On + * @return void + */ + protected function _checkAuth() + { + if (ini_get('xcache.admin.enable_auth')) { + throw new \BadMethodCallException('To use all features of \Doctrine\Common\Cache\XcacheCache, you must set "xcache.admin.enable_auth" to "Off" in your php.ini.'); + } + } +} \ No newline at end of file diff --git a/Doctrine/Common/ClassLoader.php b/Doctrine/Common/ClassLoader.php new file mode 100644 index 0000000..a1dc4be --- /dev/null +++ b/Doctrine/Common/ClassLoader.php @@ -0,0 +1,240 @@ +. + */ + +namespace Doctrine\Common; + +/** + * A ClassLoader is an autoloader for class files that can be + * installed on the SPL autoload stack. It is a class loader that either loads only classes + * of a specific namespace or all namespaces and it is suitable for working together + * with other autoloaders in the SPL autoload stack. + * + * If no include path is configured through the constructor or {@link setIncludePath}, a ClassLoader + * relies on the PHP include_path. + * + * @author Roman Borschel + * @since 2.0 + */ +class ClassLoader +{ + private $fileExtension = '.php'; + private $namespace; + private $includePath; + private $namespaceSeparator = '\\'; + + /** + * Creates a new ClassLoader that loads classes of the + * specified namespace from the specified include path. + * + * If no include path is given, the ClassLoader relies on the PHP include_path. + * If neither a namespace nor an include path is given, the ClassLoader will + * be responsible for loading all classes, thereby relying on the PHP include_path. + * + * @param string $ns The namespace of the classes to load. + * @param string $includePath The base include path to use. + */ + public function __construct($ns = null, $includePath = null) + { + $this->namespace = $ns; + $this->includePath = $includePath; + } + + /** + * Sets the namespace separator used by classes in the namespace of this ClassLoader. + * + * @param string $sep The separator to use. + */ + public function setNamespaceSeparator($sep) + { + $this->namespaceSeparator = $sep; + } + + /** + * Gets the namespace separator used by classes in the namespace of this ClassLoader. + * + * @return string + */ + public function getNamespaceSeparator() + { + return $this->namespaceSeparator; + } + + /** + * Sets the base include path for all class files in the namespace of this ClassLoader. + * + * @param string $includePath + */ + public function setIncludePath($includePath) + { + $this->includePath = $includePath; + } + + /** + * Gets the base include path for all class files in the namespace of this ClassLoader. + * + * @return string + */ + public function getIncludePath() + { + return $this->includePath; + } + + /** + * Sets the file extension of class files in the namespace of this ClassLoader. + * + * @param string $fileExtension + */ + public function setFileExtension($fileExtension) + { + $this->fileExtension = $fileExtension; + } + + /** + * Gets the file extension of class files in the namespace of this ClassLoader. + * + * @return string + */ + public function getFileExtension() + { + return $this->fileExtension; + } + + /** + * Registers this ClassLoader on the SPL autoload stack. + */ + public function register() + { + spl_autoload_register(array($this, 'loadClass')); + } + + /** + * Removes this ClassLoader from the SPL autoload stack. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $classname The name of the class to load. + * @return boolean TRUE if the class has been successfully loaded, FALSE otherwise. + */ + public function loadClass($className) + { + if ($this->namespace !== null && strpos($className, $this->namespace.$this->namespaceSeparator) !== 0) { + return false; + } + + require ($this->includePath !== null ? $this->includePath . DIRECTORY_SEPARATOR : '') + . str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) + . $this->fileExtension; + + return true; + } + + /** + * Asks this ClassLoader whether it can potentially load the class (file) with + * the given name. + * + * @param string $className The fully-qualified name of the class. + * @return boolean TRUE if this ClassLoader can load the class, FALSE otherwise. + */ + public function canLoadClass($className) + { + if ($this->namespace !== null && strpos($className, $this->namespace.$this->namespaceSeparator) !== 0) { + return false; + } + return file_exists(($this->includePath !== null ? $this->includePath . DIRECTORY_SEPARATOR : '') + . str_replace($this->namespaceSeparator, DIRECTORY_SEPARATOR, $className) + . $this->fileExtension); + } + + /** + * Checks whether a class with a given name exists. A class "exists" if it is either + * already defined in the current request or if there is an autoloader on the SPL + * autoload stack that is a) responsible for the class in question and b) is able to + * load a class file in which the class definition resides. + * + * If the class is not already defined, each autoloader in the SPL autoload stack + * is asked whether it is able to tell if the class exists. If the autoloader is + * a ClassLoader, {@link canLoadClass} is used, otherwise the autoload + * function of the autoloader is invoked and expected to return a value that + * evaluates to TRUE if the class (file) exists. As soon as one autoloader reports + * that the class exists, TRUE is returned. + * + * Note that, depending on what kinds of autoloaders are installed on the SPL + * autoload stack, the class (file) might already be loaded as a result of checking + * for its existence. This is not the case with a ClassLoader, who separates + * these responsibilities. + * + * @param string $className The fully-qualified name of the class. + * @return boolean TRUE if the class exists as per the definition given above, FALSE otherwise. + */ + public static function classExists($className) + { + if (class_exists($className, false)) { + return true; + } + + foreach (spl_autoload_functions() as $loader) { + if (is_array($loader)) { // array(???, ???) + if (is_object($loader[0])) { + if ($loader[0] instanceof ClassLoader) { // array($obj, 'methodName') + if ($loader[0]->canLoadClass($className)) { + return true; + } + } else if ($loader[0]->{$loader[1]}($className)) { + return true; + } + } else if ($loader[0]::$loader[1]($className)) { // array('ClassName', 'methodName') + return true; + } + } else if ($loader instanceof \Closure) { // function($className) {..} + if ($loader($className)) { + return true; + } + } else if (is_string($loader) && $loader($className)) { // "MyClass::loadClass" + return true; + } + } + + return false; + } + + /** + * Gets the ClassLoader from the SPL autoload stack that is responsible + * for (and is able to load) the class with the given name. + * + * @param string $className The name of the class. + * @return The ClassLoader for the class or NULL if no such ClassLoader exists. + */ + public static function getClassLoader($className) + { + foreach (spl_autoload_functions() as $loader) { + if (is_array($loader) && $loader[0] instanceof ClassLoader && + $loader[0]->canLoadClass($className)) { + return $loader[0]; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Doctrine/Common/Collections/ArrayCollection.php b/Doctrine/Common/Collections/ArrayCollection.php new file mode 100644 index 0000000..ec7de5b --- /dev/null +++ b/Doctrine/Common/Collections/ArrayCollection.php @@ -0,0 +1,438 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Closure, ArrayIterator; + +/** + * An ArrayCollection is a Collection implementation that wraps a regular PHP array. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ArrayCollection implements Collection +{ + /** + * An array containing the entries of this collection. + * + * @var array + */ + private $_elements; + + /** + * Initializes a new ArrayCollection. + * + * @param array $elements + */ + public function __construct(array $elements = array()) + { + $this->_elements = $elements; + } + + /** + * Gets the PHP array representation of this collection. + * + * @return array The PHP array representation of this collection. + */ + public function toArray() + { + return $this->_elements; + } + + /** + * Sets the internal iterator to the first element in the collection and + * returns this element. + * + * @return mixed + */ + public function first() + { + return reset($this->_elements); + } + + /** + * Sets the internal iterator to the last element in the collection and + * returns this element. + * + * @return mixed + */ + public function last() + { + return end($this->_elements); + } + + /** + * Gets the current key/index at the current internal iterator position. + * + * @return mixed + */ + public function key() + { + return key($this->_elements); + } + + /** + * Moves the internal iterator position to the next element. + * + * @return mixed + */ + public function next() + { + return next($this->_elements); + } + + /** + * Gets the element of the collection at the current internal iterator position. + * + * @return mixed + */ + public function current() + { + return current($this->_elements); + } + + /** + * Removes an element with a specific key/index from the collection. + * + * @param mixed $key + * @return mixed The removed element or NULL, if no element exists for the given key. + */ + public function remove($key) + { + if (isset($this->_elements[$key])) { + $removed = $this->_elements[$key]; + unset($this->_elements[$key]); + + return $removed; + } + + return null; + } + + /** + * Removes the specified element from the collection, if it is found. + * + * @param mixed $element The element to remove. + * @return boolean TRUE if this collection contained the specified element, FALSE otherwise. + */ + public function removeElement($element) + { + $key = array_search($element, $this->_elements, true); + + if ($key !== false) { + unset($this->_elements[$key]); + + return true; + } + + return false; + } + + /** + * ArrayAccess implementation of offsetExists() + * + * @see containsKey() + */ + public function offsetExists($offset) + { + return $this->containsKey($offset); + } + + /** + * ArrayAccess implementation of offsetGet() + * + * @see get() + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * ArrayAccess implementation of offsetGet() + * + * @see add() + * @see set() + */ + public function offsetSet($offset, $value) + { + if ( ! isset($offset)) { + return $this->add($value); + } + return $this->set($offset, $value); + } + + /** + * ArrayAccess implementation of offsetUnset() + * + * @see remove() + */ + public function offsetUnset($offset) + { + return $this->remove($offset); + } + + /** + * Checks whether the collection contains a specific key/index. + * + * @param mixed $key The key to check for. + * @return boolean TRUE if the given key/index exists, FALSE otherwise. + */ + public function containsKey($key) + { + return isset($this->_elements[$key]); + } + + /** + * Checks whether the given element is contained in the collection. + * Only element values are compared, not keys. The comparison of two elements + * is strict, that means not only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element + * @return boolean TRUE if the given element is contained in the collection, + * FALSE otherwise. + */ + public function contains($element) + { + return in_array($element, $this->_elements, true); + } + + /** + * Tests for the existance of an element that satisfies the given predicate. + * + * @param Closure $p The predicate. + * @return boolean TRUE if the predicate is TRUE for at least one element, FALSE otherwise. + */ + public function exists(Closure $p) + { + foreach ($this->_elements as $key => $element) + if ($p($key, $element)) return true; + return false; + } + + /** + * Searches for a given element and, if found, returns the corresponding key/index + * of that element. The comparison of two elements is strict, that means not + * only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element The element to search for. + * @return mixed The key/index of the element or FALSE if the element was not found. + */ + public function indexOf($element) + { + return array_search($element, $this->_elements, true); + } + + /** + * Gets the element with the given key/index. + * + * @param mixed $key The key. + * @return mixed The element or NULL, if no element exists for the given key. + */ + public function get($key) + { + if (isset($this->_elements[$key])) { + return $this->_elements[$key]; + } + return null; + } + + /** + * Gets all keys/indexes of the collection elements. + * + * @return array + */ + public function getKeys() + { + return array_keys($this->_elements); + } + + /** + * Gets all elements. + * + * @return array + */ + public function getValues() + { + return array_values($this->_elements); + } + + /** + * Returns the number of elements in the collection. + * + * Implementation of the Countable interface. + * + * @return integer The number of elements in the collection. + */ + public function count() + { + return count($this->_elements); + } + + /** + * Adds/sets an element in the collection at the index / with the specified key. + * + * When the collection is a Map this is like put(key,value)/add(key,value). + * When the collection is a List this is like add(position,value). + * + * @param mixed $key + * @param mixed $value + */ + public function set($key, $value) + { + $this->_elements[$key] = $value; + } + + /** + * Adds an element to the collection. + * + * @param mixed $value + * @return boolean Always TRUE. + */ + public function add($value) + { + $this->_elements[] = $value; + return true; + } + + /** + * Checks whether the collection is empty. + * + * Note: This is preferrable over count() == 0. + * + * @return boolean TRUE if the collection is empty, FALSE otherwise. + */ + public function isEmpty() + { + return ! $this->_elements; + } + + /** + * Gets an iterator for iterating over the elements in the collection. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new ArrayIterator($this->_elements); + } + + /** + * Applies the given function to each element in the collection and returns + * a new collection with the elements returned by the function. + * + * @param Closure $func + * @return Collection + */ + public function map(Closure $func) + { + return new ArrayCollection(array_map($func, $this->_elements)); + } + + /** + * Returns all the elements of this collection that satisfy the predicate p. + * The order of the elements is preserved. + * + * @param Closure $p The predicate used for filtering. + * @return Collection A collection with the results of the filter operation. + */ + public function filter(Closure $p) + { + return new ArrayCollection(array_filter($this->_elements, $p)); + } + + /** + * Applies the given predicate p to all elements of this collection, + * returning true, if the predicate yields true for all elements. + * + * @param Closure $p The predicate. + * @return boolean TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + public function forAll(Closure $p) + { + foreach ($this->_elements as $key => $element) { + if ( ! $p($key, $element)) { + return false; + } + } + + return true; + } + + /** + * Partitions this collection in two collections according to a predicate. + * Keys are preserved in the resulting collections. + * + * @param Closure $p The predicate on which to partition. + * @return array An array with two elements. The first element contains the collection + * of elements where the predicate returned TRUE, the second element + * contains the collection of elements where the predicate returned FALSE. + */ + public function partition(Closure $p) + { + $coll1 = $coll2 = array(); + foreach ($this->_elements as $key => $element) { + if ($p($key, $element)) { + $coll1[$key] = $element; + } else { + $coll2[$key] = $element; + } + } + return array(new ArrayCollection($coll1), new ArrayCollection($coll2)); + } + + /** + * Returns a string representation of this object. + * + * @return string + */ + public function __toString() + { + return __CLASS__ . '@' . spl_object_hash($this); + } + + /** + * Clears the collection. + */ + public function clear() + { + $this->_elements = array(); + } + + /** + * Extract a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset + * @param int $length + * @return array + */ + public function slice($offset, $length = null) + { + return array_slice($this->_elements, $offset, $length, true); + } +} diff --git a/Doctrine/Common/Collections/Collection.php b/Doctrine/Common/Collections/Collection.php new file mode 100644 index 0000000..9a6300d --- /dev/null +++ b/Doctrine/Common/Collections/Collection.php @@ -0,0 +1,243 @@ +. + */ + +namespace Doctrine\Common\Collections; + +use Closure, Countable, IteratorAggregate, ArrayAccess; + +/** + * The missing (SPL) Collection/Array/OrderedMap interface. + * + * A Collection resembles the nature of a regular PHP array. That is, + * it is essentially an ordered map that can also be used + * like a list. + * + * A Collection has an internal iterator just like a PHP array. In addition, + * a Collection can be iterated with external iterators, which is preferrable. + * To use an external iterator simply use the foreach language construct to + * iterate over the collection (which calls {@link getIterator()} internally) or + * explicitly retrieve an iterator though {@link getIterator()} which can then be + * used to iterate over the collection. + * You can not rely on the internal iterator of the collection being at a certain + * position unless you explicitly positioned it before. Prefer iteration with + * external iterators. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface Collection extends Countable, IteratorAggregate, ArrayAccess +{ + /** + * Adds an element at the end of the collection. + * + * @param mixed $element The element to add. + * @return boolean Always TRUE. + */ + function add($element); + + /** + * Clears the collection, removing all elements. + */ + function clear(); + + /** + * Checks whether an element is contained in the collection. + * This is an O(n) operation, where n is the size of the collection. + * + * @param mixed $element The element to search for. + * @return boolean TRUE if the collection contains the element, FALSE otherwise. + */ + function contains($element); + + /** + * Checks whether the collection is empty (contains no elements). + * + * @return boolean TRUE if the collection is empty, FALSE otherwise. + */ + function isEmpty(); + + /** + * Removes the element at the specified index from the collection. + * + * @param string|integer $key The kex/index of the element to remove. + * @return mixed The removed element or NULL, if the collection did not contain the element. + */ + function remove($key); + + /** + * Removes the specified element from the collection, if it is found. + * + * @param mixed $element The element to remove. + * @return boolean TRUE if this collection contained the specified element, FALSE otherwise. + */ + function removeElement($element); + + /** + * Checks whether the collection contains an element with the specified key/index. + * + * @param string|integer $key The key/index to check for. + * @return boolean TRUE if the collection contains an element with the specified key/index, + * FALSE otherwise. + */ + function containsKey($key); + + /** + * Gets the element at the specified key/index. + * + * @param string|integer $key The key/index of the element to retrieve. + * @return mixed + */ + function get($key); + + /** + * Gets all keys/indices of the collection. + * + * @return array The keys/indices of the collection, in the order of the corresponding + * elements in the collection. + */ + function getKeys(); + + /** + * Gets all values of the collection. + * + * @return array The values of all elements in the collection, in the order they + * appear in the collection. + */ + function getValues(); + + /** + * Sets an element in the collection at the specified key/index. + * + * @param string|integer $key The key/index of the element to set. + * @param mixed $value The element to set. + */ + function set($key, $value); + + /** + * Gets a native PHP array representation of the collection. + * + * @return array + */ + function toArray(); + + /** + * Sets the internal iterator to the first element in the collection and + * returns this element. + * + * @return mixed + */ + function first(); + + /** + * Sets the internal iterator to the last element in the collection and + * returns this element. + * + * @return mixed + */ + function last(); + + /** + * Gets the key/index of the element at the current iterator position. + * + */ + function key(); + + /** + * Gets the element of the collection at the current iterator position. + * + */ + function current(); + + /** + * Moves the internal iterator position to the next element. + * + */ + function next(); + + /** + * Tests for the existence of an element that satisfies the given predicate. + * + * @param Closure $p The predicate. + * @return boolean TRUE if the predicate is TRUE for at least one element, FALSE otherwise. + */ + function exists(Closure $p); + + /** + * Returns all the elements of this collection that satisfy the predicate p. + * The order of the elements is preserved. + * + * @param Closure $p The predicate used for filtering. + * @return Collection A collection with the results of the filter operation. + */ + function filter(Closure $p); + + /** + * Applies the given predicate p to all elements of this collection, + * returning true, if the predicate yields true for all elements. + * + * @param Closure $p The predicate. + * @return boolean TRUE, if the predicate yields TRUE for all elements, FALSE otherwise. + */ + function forAll(Closure $p); + + /** + * Applies the given function to each element in the collection and returns + * a new collection with the elements returned by the function. + * + * @param Closure $func + * @return Collection + */ + function map(Closure $func); + + /** + * Partitions this collection in two collections according to a predicate. + * Keys are preserved in the resulting collections. + * + * @param Closure $p The predicate on which to partition. + * @return array An array with two elements. The first element contains the collection + * of elements where the predicate returned TRUE, the second element + * contains the collection of elements where the predicate returned FALSE. + */ + function partition(Closure $p); + + /** + * Gets the index/key of a given element. The comparison of two elements is strict, + * that means not only the value but also the type must match. + * For objects this means reference equality. + * + * @param mixed $element The element to search for. + * @return mixed The key/index of the element or FALSE if the element was not found. + */ + function indexOf($element); + + /** + * Extract a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset + * @param int $length + * @return array + */ + public function slice($offset, $length = null); +} \ No newline at end of file diff --git a/Doctrine/Common/CommonException.php b/Doctrine/Common/CommonException.php new file mode 100644 index 0000000..8c5669b --- /dev/null +++ b/Doctrine/Common/CommonException.php @@ -0,0 +1,28 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Base exception class for package Doctrine\Common + * @author heinrich + * + */ +class CommonException extends \Exception { +} \ No newline at end of file diff --git a/Doctrine/Common/EventArgs.php b/Doctrine/Common/EventArgs.php new file mode 100644 index 0000000..6a3c069 --- /dev/null +++ b/Doctrine/Common/EventArgs.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\Common; + +/** + * EventArgs is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass state + * information to an event handler when an event is raised. The single empty EventArgs + * instance can be obtained through {@link getEmptyInstance}. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EventArgs +{ + /** + * @var EventArgs Single instance of EventArgs + * @static + */ + private static $_emptyEventArgsInstance; + + /** + * Gets the single, empty and immutable EventArgs instance. + * + * This instance will be used when events are dispatched without any parameter, + * like this: EventManager::dispatchEvent('eventname'); + * + * The benefit from this is that only one empty instance is instantiated and shared + * (otherwise there would be instances for every dispatched in the abovementioned form) + * + * @see EventManager::dispatchEvent + * @link http://msdn.microsoft.com/en-us/library/system.eventargs.aspx + * @static + * @return EventArgs + */ + public static function getEmptyInstance() + { + if ( ! self::$_emptyEventArgsInstance) { + self::$_emptyEventArgsInstance = new EventArgs; + } + + return self::$_emptyEventArgsInstance; + } +} diff --git a/Doctrine/Common/EventManager.php b/Doctrine/Common/EventManager.php new file mode 100644 index 0000000..fa98cf2 --- /dev/null +++ b/Doctrine/Common/EventManager.php @@ -0,0 +1,138 @@ +. + */ + +namespace Doctrine\Common; + +use Doctrine\Common\Events\Event; + +/** + * The EventManager is the central point of Doctrine's event listener system. + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EventManager +{ + /** + * Map of registered listeners. + * => + * + * @var array + */ + private $_listeners = array(); + + /** + * Dispatches an event to all registered listeners. + * + * @param string $eventName The name of the event to dispatch. The name of the event is + * the name of the method that is invoked on listeners. + * @param EventArgs $eventArgs The event arguments to pass to the event handlers/listeners. + * If not supplied, the single empty EventArgs instance is used. + * @return boolean + */ + public function dispatchEvent($eventName, EventArgs $eventArgs = null) + { + if (isset($this->_listeners[$eventName])) { + $eventArgs = $eventArgs === null ? EventArgs::getEmptyInstance() : $eventArgs; + + foreach ($this->_listeners[$eventName] as $listener) { + $listener->$eventName($eventArgs); + } + } + } + + /** + * Gets the listeners of a specific event or all listeners. + * + * @param string $event The name of the event. + * @return array The event listeners for the specified event, or all event listeners. + */ + public function getListeners($event = null) + { + return $event ? $this->_listeners[$event] : $this->_listeners; + } + + /** + * Checks whether an event has any registered listeners. + * + * @param string $event + * @return boolean TRUE if the specified event has any listeners, FALSE otherwise. + */ + public function hasListeners($event) + { + return isset($this->_listeners[$event]) && $this->_listeners[$event]; + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string|array $events The event(s) to listen on. + * @param object $listener The listener object. + */ + public function addEventListener($events, $listener) + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Overrides listener if a previous one was associated already + // Prevents duplicate listeners on same event (same instance only) + $this->_listeners[$event][$hash] = $listener; + } + } + + /** + * Removes an event listener from the specified events. + * + * @param string|array $events + * @param object $listener + */ + public function removeEventListener($events, $listener) + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Check if actually have this listener associated + if (isset($this->_listeners[$event][$hash])) { + unset($this->_listeners[$event][$hash]); + } + } + } + + /** + * Adds an EventSubscriber. The subscriber is asked for all the events he is + * interested in and added as a listener for these events. + * + * @param Doctrine\Common\EventSubscriber $subscriber The subscriber. + */ + public function addEventSubscriber(EventSubscriber $subscriber) + { + $this->addEventListener($subscriber->getSubscribedEvents(), $subscriber); + } +} \ No newline at end of file diff --git a/Doctrine/Common/EventSubscriber.php b/Doctrine/Common/EventSubscriber.php new file mode 100644 index 0000000..8e55973 --- /dev/null +++ b/Doctrine/Common/EventSubscriber.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\Common; + +/** + * An EventSubscriber knows himself what events he is interested in. + * If an EventSubscriber is added to an EventManager, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface EventSubscriber +{ + /** + * Returns an array of events this subscriber wants to listen to. + * + * @return array + */ + public function getSubscribedEvents(); +} diff --git a/Doctrine/Common/Lexer.php b/Doctrine/Common/Lexer.php new file mode 100644 index 0000000..c19e34e --- /dev/null +++ b/Doctrine/Common/Lexer.php @@ -0,0 +1,255 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Base class for writing simple lexers, i.e. for creating small DSLs. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @todo Rename: AbstractLexer + */ +abstract class Lexer +{ + /** + * @var array Array of scanned tokens + */ + private $tokens = array(); + + /** + * @var integer Current lexer position in input string + */ + private $position = 0; + + /** + * @var integer Current peek of current lexer position + */ + private $peek = 0; + + /** + * @var array The next token in the input. + */ + public $lookahead; + + /** + * @var array The last matched/seen token. + */ + public $token; + + /** + * Sets the input data to be tokenized. + * + * The Lexer is immediately reset and the new input tokenized. + * Any unprocessed tokens from any previous input are lost. + * + * @param string $input The input to be tokenized. + */ + public function setInput($input) + { + $this->tokens = array(); + $this->reset(); + $this->scan($input); + } + + /** + * Resets the lexer. + */ + public function reset() + { + $this->lookahead = null; + $this->token = null; + $this->peek = 0; + $this->position = 0; + } + + /** + * Resets the peek pointer to 0. + */ + public function resetPeek() + { + $this->peek = 0; + } + + /** + * Resets the lexer position on the input to the given position. + * + * @param integer $position Position to place the lexical scanner + */ + public function resetPosition($position = 0) + { + $this->position = $position; + } + + /** + * Checks whether a given token matches the current lookahead. + * + * @param integer|string $token + * @return boolean + */ + public function isNextToken($token) + { + return $this->lookahead['type'] === $token; + } + + /** + * Moves to the next token in the input string. + * + * A token is an associative array containing three items: + * - 'value' : the string value of the token in the input string + * - 'type' : the type of the token (identifier, numeric, string, input + * parameter, none) + * - 'position' : the position of the token in the input string + * + * @return array|null the next token; null if there is no more tokens left + */ + public function moveNext() + { + $this->peek = 0; + $this->token = $this->lookahead; + $this->lookahead = (isset($this->tokens[$this->position])) + ? $this->tokens[$this->position++] : null; + + return $this->lookahead !== null; + } + + /** + * Tells the lexer to skip input tokens until it sees a token with the given value. + * + * @param $type The token type to skip until. + */ + public function skipUntil($type) + { + while ($this->lookahead !== null && $this->lookahead['type'] !== $type) { + $this->moveNext(); + } + } + + /** + * Checks if given value is identical to the given token + * + * @param mixed $value + * @param integer $token + * @return boolean + */ + public function isA($value, $token) + { + return $this->getType($value) === $token; + } + + /** + * Moves the lookahead token forward. + * + * @return array | null The next token or NULL if there are no more tokens ahead. + */ + public function peek() + { + if (isset($this->tokens[$this->position + $this->peek])) { + return $this->tokens[$this->position + $this->peek++]; + } else { + return null; + } + } + + /** + * Peeks at the next token, returns it and immediately resets the peek. + * + * @return array|null The next token or NULL if there are no more tokens ahead. + */ + public function glimpse() + { + $peek = $this->peek(); + $this->peek = 0; + return $peek; + } + + /** + * Scans the input string for tokens. + * + * @param string $input a query string + */ + protected function scan($input) + { + static $regex; + + if ( ! isset($regex)) { + $regex = '/(' . implode(')|(', $this->getCatchablePatterns()) . ')|' + . implode('|', $this->getNonCatchablePatterns()) . '/i'; + } + + $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; + $matches = preg_split($regex, $input, -1, $flags); + + foreach ($matches as $match) { + // Must remain before 'value' assignment since it can change content + $type = $this->getType($match[0]); + + $this->tokens[] = array( + 'value' => $match[0], + 'type' => $type, + 'position' => $match[1], + ); + } + } + + /** + * Gets the literal for a given token. + * + * @param integer $token + * @return string + */ + public function getLiteral($token) + { + $className = get_class($this); + $reflClass = new \ReflectionClass($className); + $constants = $reflClass->getConstants(); + + foreach ($constants as $name => $value) { + if ($value === $token) { + return $className . '::' . $name; + } + } + + return $token; + } + + /** + * Lexical catchable patterns. + * + * @return array + */ + abstract protected function getCatchablePatterns(); + + /** + * Lexical non-catchable patterns. + * + * @return array + */ + abstract protected function getNonCatchablePatterns(); + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * @return integer + */ + abstract protected function getType(&$value); +} \ No newline at end of file diff --git a/Doctrine/Common/NotifyPropertyChanged.php b/Doctrine/Common/NotifyPropertyChanged.php new file mode 100644 index 0000000..93e504a --- /dev/null +++ b/Doctrine/Common/NotifyPropertyChanged.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Contract for classes that provide the service of notifying listeners of + * changes to their properties. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface NotifyPropertyChanged +{ + /** + * Adds a listener that wants to be notified about property changes. + * + * @param PropertyChangedListener $listener + */ + function addPropertyChangedListener(PropertyChangedListener $listener); +} + diff --git a/Doctrine/Common/PropertyChangedListener.php b/Doctrine/Common/PropertyChangedListener.php new file mode 100644 index 0000000..87c5b41 --- /dev/null +++ b/Doctrine/Common/PropertyChangedListener.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Contract for classes that are potential listeners of a NotifyPropertyChanged + * implementor. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface PropertyChangedListener +{ + /** + * Notifies the listener of a property change. + * + * @param object $sender The object on which the property changed. + * @param string $propertyName The name of the property that changed. + * @param mixed $oldValue The old value of the property that changed. + * @param mixed $newValue The new value of the property that changed. + */ + function propertyChanged($sender, $propertyName, $oldValue, $newValue); +} + diff --git a/Doctrine/Common/Util/Debug.php b/Doctrine/Common/Util/Debug.php new file mode 100644 index 0000000..735dbf3 --- /dev/null +++ b/Doctrine/Common/Util/Debug.php @@ -0,0 +1,136 @@ +. + */ + +namespace Doctrine\Common\Util; + +/** + * Static class containing most used debug methods. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Giorgio Sironi + */ +final class Debug +{ + /** + * Private constructor (prevents from instantiation) + * + */ + private function __construct() {} + + /** + * Prints a dump of the public, protected and private properties of $var. + * + * @static + * @link http://xdebug.org/ + * @param mixed $var + * @param integer $maxDepth Maximum nesting level for object properties + */ + public static function dump($var, $maxDepth = 2) + { + ini_set('html_errors', 'On'); + + if (extension_loaded('xdebug')) { + ini_set('xdebug.var_display_max_depth', $maxDepth); + } + + $var = self::export($var, $maxDepth++); + + ob_start(); + var_dump($var); + $dump = ob_get_contents(); + ob_end_clean(); + + echo strip_tags(html_entity_decode($dump)); + + ini_set('html_errors', 'Off'); + } + + public static function export($var, $maxDepth) + { + $return = null; + $isObj = is_object($var); + + if ($isObj && in_array('Doctrine\Common\Collections\Collection', class_implements($var))) { + $var = $var->toArray(); + } + + if ($maxDepth) { + if (is_array($var)) { + $return = array(); + + foreach ($var as $k => $v) { + $return[$k] = self::export($v, $maxDepth - 1); + } + } else if ($isObj) { + if ($var instanceof \DateTime) { + $return = $var->format('c'); + } else { + $reflClass = new \ReflectionClass(get_class($var)); + $return = new \stdclass(); + $return->{'__CLASS__'} = get_class($var); + + if ($var instanceof \Doctrine\ORM\Proxy\Proxy && ! $var->__isInitialized__) { + $reflProperty = $reflClass->getProperty('_identifier'); + $reflProperty->setAccessible(true); + + foreach ($reflProperty->getValue($var) as $name => $value) { + $return->$name = self::export($value, $maxDepth - 1); + } + } else { + $excludeProperties = array(); + + if ($var instanceof \Doctrine\ORM\Proxy\Proxy) { + $excludeProperties = array('_entityPersister', '__isInitialized__', '_identifier'); + } + + foreach ($reflClass->getProperties() as $reflProperty) { + $name = $reflProperty->getName(); + + if ( ! in_array($name, $excludeProperties)) { + $reflProperty->setAccessible(true); + + $return->$name = self::export($reflProperty->getValue($var), $maxDepth - 1); + } + } + } + } + } else { + $return = $var; + } + } else { + $return = is_object($var) ? get_class($var) + : (is_array($var) ? 'Array(' . count($var) . ')' : $var); + } + + return $return; + } + + public static function toString($obj) + { + return method_exists('__toString', $obj) ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj); + } +} diff --git a/Doctrine/Common/Util/Inflector.php b/Doctrine/Common/Util/Inflector.php new file mode 100644 index 0000000..78e5709 --- /dev/null +++ b/Doctrine/Common/Util/Inflector.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\Common\Util; + +/** + * Doctrine inflector has static methods for inflecting text + * + * The methods in these classes are from several different sources collected + * across several different php projects and several different authors. The + * original author names and emails are not known + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 1.0 + * @version $Revision: 3189 $ + * @author Konsta Vesterinen + * @author Jonathan H. Wage + */ +class Inflector +{ + /** + * Convert word in to the format for a Doctrine table name. Converts 'ModelName' to 'model_name' + * + * @param string $word Word to tableize + * @return string $word Tableized word + */ + public static function tableize($word) + { + return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word)); + } + + /** + * Convert a word in to the format for a Doctrine class name. Converts 'table_name' to 'TableName' + * + * @param string $word Word to classify + * @return string $word Classified word + */ + public static function classify($word) + { + return str_replace(" ", "", ucwords(strtr($word, "_-", " "))); + } + + /** + * Camelize a word. This uses the classify() method and turns the first character to lowercase + * + * @param string $word + * @return string $word + */ + public static function camelize($word) + { + return lcfirst(self::classify($word)); + } +} \ No newline at end of file diff --git a/Doctrine/Common/Version.php b/Doctrine/Common/Version.php new file mode 100644 index 0000000..bc05e6e --- /dev/null +++ b/Doctrine/Common/Version.php @@ -0,0 +1,55 @@ +. + */ + +namespace Doctrine\Common; + +/** + * Class to store and retrieve the version of Doctrine + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Version +{ + /** + * Current Doctrine Version + */ + const VERSION = '2.0.0RC2-DEV'; + + /** + * Compares a Doctrine version with the current one. + * + * @param string $version Doctrine version to compare. + * @return int Returns -1 if older, 0 if it is the same, 1 if version + * passed as argument is newer. + */ + public static function compare($version) + { + $currentVersion = str_replace(' ', '', strtolower(self::VERSION)); + $version = str_replace(' ', '', $version); + + return version_compare($version, $currentVersion); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Configuration.php b/Doctrine/DBAL/Configuration.php new file mode 100644 index 0000000..76ce1c4 --- /dev/null +++ b/Doctrine/DBAL/Configuration.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\DBAL; + +use Doctrine\DBAL\Logging\SQLLogger; + +/** + * Configuration container for the Doctrine DBAL. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @internal When adding a new configuration option just write a getter/setter + * pair and add the option to the _attributes array with a proper default value. + */ +class Configuration +{ + /** + * The attributes that are contained in the configuration. + * Values are default values. + * + * @var array + */ + protected $_attributes = array(); + + /** + * Sets the SQL logger to use. Defaults to NULL which means SQL logging is disabled. + * + * @param SQLLogger $logger + */ + public function setSQLLogger(SQLLogger $logger) + { + $this->_attributes['sqlLogger'] = $logger; + } + + /** + * Gets the SQL logger that is used. + * + * @return SQLLogger + */ + public function getSQLLogger() + { + return isset($this->_attributes['sqlLogger']) ? + $this->_attributes['sqlLogger'] : null; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Connection.php b/Doctrine/DBAL/Connection.php new file mode 100644 index 0000000..1bf7ff7 --- /dev/null +++ b/Doctrine/DBAL/Connection.php @@ -0,0 +1,1052 @@ +. + */ + +namespace Doctrine\DBAL; + +use PDO, Closure, Exception, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Driver\Connection as DriverConnection, + Doctrine\Common\EventManager, + Doctrine\DBAL\DBALException; + +/** + * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like + * events, transaction isolation levels, configuration, emulated transaction nesting, + * lazy connecting and more. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Konsta Vesterinen + * @author Lukas Smith (MDB2 library) + * @author Benjamin Eberlei + */ +class Connection implements DriverConnection +{ + /** + * Constant for transaction isolation level READ UNCOMMITTED. + */ + const TRANSACTION_READ_UNCOMMITTED = 1; + + /** + * Constant for transaction isolation level READ COMMITTED. + */ + const TRANSACTION_READ_COMMITTED = 2; + + /** + * Constant for transaction isolation level REPEATABLE READ. + */ + const TRANSACTION_REPEATABLE_READ = 3; + + /** + * Constant for transaction isolation level SERIALIZABLE. + */ + const TRANSACTION_SERIALIZABLE = 4; + + /** + * The wrapped driver connection. + * + * @var Doctrine\DBAL\Driver\Connection + */ + protected $_conn; + + /** + * @var Doctrine\DBAL\Configuration + */ + protected $_config; + + /** + * @var Doctrine\Common\EventManager + */ + protected $_eventManager; + + /** + * Whether or not a connection has been established. + * + * @var boolean + */ + private $_isConnected = false; + + /** + * The transaction nesting level. + * + * @var integer + */ + private $_transactionNestingLevel = 0; + + /** + * The currently active transaction isolation level. + * + * @var integer + */ + private $_transactionIsolationLevel; + + /** + * If nested transations should use savepoints + * + * @var integer + */ + private $_nestTransactionsWithSavepoints; + + /** + * The parameters used during creation of the Connection instance. + * + * @var array + */ + private $_params = array(); + + /** + * The DatabasePlatform object that provides information about the + * database platform used by the connection. + * + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + /** + * The schema manager. + * + * @var Doctrine\DBAL\Schema\SchemaManager + */ + protected $_schemaManager; + + /** + * The used DBAL driver. + * + * @var Doctrine\DBAL\Driver + */ + protected $_driver; + + /** + * Flag that indicates whether the current transaction is marked for rollback only. + * + * @var boolean + */ + private $_isRollbackOnly = false; + + /** + * Initializes a new instance of the Connection class. + * + * @param array $params The connection parameters. + * @param Driver $driver + * @param Configuration $config + * @param EventManager $eventManager + */ + public function __construct(array $params, Driver $driver, Configuration $config = null, + EventManager $eventManager = null) + { + $this->_driver = $driver; + $this->_params = $params; + + if (isset($params['pdo'])) { + $this->_conn = $params['pdo']; + $this->_isConnected = true; + } + + // Create default config and event manager if none given + if ( ! $config) { + $config = new Configuration(); + } + + if ( ! $eventManager) { + $eventManager = new EventManager(); + } + + $this->_config = $config; + $this->_eventManager = $eventManager; + if ( ! isset($params['platform'])) { + $this->_platform = $driver->getDatabasePlatform(); + } else if ($params['platform'] instanceof Platforms\AbstractPlatform) { + $this->_platform = $params['platform']; + } else { + throw DBALException::invalidPlatformSpecified(); + } + $this->_transactionIsolationLevel = $this->_platform->getDefaultTransactionIsolationLevel(); + } + + /** + * Gets the parameters used during instantiation. + * + * @return array $params + */ + public function getParams() + { + return $this->_params; + } + + /** + * Gets the name of the database this Connection is connected to. + * + * @return string $database + */ + public function getDatabase() + { + return $this->_driver->getDatabase($this); + } + + /** + * Gets the hostname of the currently connected database. + * + * @return string + */ + public function getHost() + { + return isset($this->_params['host']) ? $this->_params['host'] : null; + } + + /** + * Gets the port of the currently connected database. + * + * @return mixed + */ + public function getPort() + { + return isset($this->_params['port']) ? $this->_params['port'] : null; + } + + /** + * Gets the username used by this connection. + * + * @return string + */ + public function getUsername() + { + return isset($this->_params['user']) ? $this->_params['user'] : null; + } + + /** + * Gets the password used by this connection. + * + * @return string + */ + public function getPassword() + { + return isset($this->_params['password']) ? $this->_params['password'] : null; + } + + /** + * Gets the DBAL driver instance. + * + * @return Doctrine\DBAL\Driver + */ + public function getDriver() + { + return $this->_driver; + } + + /** + * Gets the Configuration used by the Connection. + * + * @return Doctrine\DBAL\Configuration + */ + public function getConfiguration() + { + return $this->_config; + } + + /** + * Gets the EventManager used by the Connection. + * + * @return Doctrine\Common\EventManager + */ + public function getEventManager() + { + return $this->_eventManager; + } + + /** + * Gets the DatabasePlatform for the connection. + * + * @return Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_platform; + } + + /** + * Establishes the connection with the database. + * + * @return boolean TRUE if the connection was successfully established, FALSE if + * the connection is already open. + */ + public function connect() + { + if ($this->_isConnected) return false; + + $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->_conn = $this->_driver->connect($this->_params, $user, $password, $driverOptions); + $this->_isConnected = true; + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + $eventArgs = new Event\ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as an associative array. + * + * @param string $statement The SQL query. + * @param array $params The query parameters. + * @return array + */ + public function fetchAssoc($statement, array $params = array()) + { + return $this->executeQuery($statement, $params)->fetch(PDO::FETCH_ASSOC); + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as a numerically indexed array. + * + * @param string $statement sql query to be executed + * @param array $params prepared statement params + * @return array + */ + public function fetchArray($statement, array $params = array()) + { + return $this->executeQuery($statement, $params)->fetch(PDO::FETCH_NUM); + } + + /** + * Prepares and executes an SQL query and returns the value of a single column + * of the first row of the result. + * + * @param string $statement sql query to be executed + * @param array $params prepared statement params + * @param int $colnum 0-indexed column number to retrieve + * @return mixed + */ + public function fetchColumn($statement, array $params = array(), $colnum = 0) + { + return $this->executeQuery($statement, $params)->fetchColumn($colnum); + } + + /** + * Whether an actual connection to the database is established. + * + * @return boolean + */ + public function isConnected() + { + return $this->_isConnected; + } + + /** + * Checks whether a transaction is currently active. + * + * @return boolean TRUE if a transaction is currently active, FALSE otherwise. + */ + public function isTransactionActive() + { + return $this->_transactionNestingLevel > 0; + } + + /** + * Executes an SQL DELETE statement on a table. + * + * @param string $table The name of the table on which to delete. + * @param array $identifier The deletion criteria. An associateve array containing column-value pairs. + * @return integer The number of affected rows. + */ + public function delete($tableName, array $identifier) + { + $this->connect(); + + $criteria = array(); + + foreach (array_keys($identifier) as $columnName) { + $criteria[] = $columnName . ' = ?'; + } + + $query = 'DELETE FROM ' . $tableName . ' WHERE ' . implode(' AND ', $criteria); + + return $this->executeUpdate($query, array_values($identifier)); + } + + /** + * Closes the connection. + * + * @return void + */ + public function close() + { + unset($this->_conn); + + $this->_isConnected = false; + } + + /** + * Sets the transaction isolation level. + * + * @param integer $level The level to set. + */ + public function setTransactionIsolation($level) + { + $this->_transactionIsolationLevel = $level; + + return $this->executeUpdate($this->_platform->getSetTransactionIsolationSQL($level)); + } + + /** + * Gets the currently active transaction isolation level. + * + * @return integer The current transaction isolation level. + */ + public function getTransactionIsolation() + { + return $this->_transactionIsolationLevel; + } + + /** + * Executes an SQL UPDATE statement on a table. + * + * @param string $table The name of the table to update. + * @param array $identifier The update criteria. An associative array containing column-value pairs. + * @return integer The number of affected rows. + */ + public function update($tableName, array $data, array $identifier) + { + $this->connect(); + $set = array(); + foreach ($data as $columnName => $value) { + $set[] = $columnName . ' = ?'; + } + + $params = array_merge(array_values($data), array_values($identifier)); + + $sql = 'UPDATE ' . $tableName . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', array_keys($identifier)) + . ' = ?'; + + return $this->executeUpdate($sql, $params); + } + + /** + * Inserts a table row with specified data. + * + * @param string $table The name of the table to insert data into. + * @param array $data An associative array containing column-value pairs. + * @return integer The number of affected rows. + */ + public function insert($tableName, array $data) + { + $this->connect(); + + // column names are specified as array keys + $cols = array(); + $placeholders = array(); + + foreach ($data as $columnName => $value) { + $cols[] = $columnName; + $placeholders[] = '?'; + } + + $query = 'INSERT INTO ' . $tableName + . ' (' . implode(', ', $cols) . ')' + . ' VALUES (' . implode(', ', $placeholders) . ')'; + + return $this->executeUpdate($query, array_values($data)); + } + + /** + * Sets the given charset on the current connection. + * + * @param string $charset The charset to set. + */ + public function setCharset($charset) + { + $this->executeUpdate($this->_platform->getSetCharsetSQL($charset)); + } + + /** + * Quote a string so it can be safely used as a table or column name, even if + * it is a reserved name. + * + * Delimiting style depends on the underlying database platform that is being used. + * + * NOTE: Just because you CAN use quoted identifiers does not mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str The name to be quoted. + * @return string The quoted name. + */ + public function quoteIdentifier($str) + { + return $this->_platform->quoteIdentifier($str); + } + + /** + * Quotes a given input parameter. + * + * @param mixed $input Parameter to be quoted. + * @param string $type Type of the parameter. + * @return string The quoted parameter. + */ + public function quote($input, $type = null) + { + $this->connect(); + + return $this->_conn->quote($input, $type); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array. + * + * @param string $sql The SQL query. + * @param array $params The query parameters. + * @return array + */ + public function fetchAll($sql, array $params = array()) + { + return $this->executeQuery($sql, $params)->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Prepares an SQL statement. + * + * @param string $statement The SQL statement to prepare. + * @return Doctrine\DBAL\Driver\Statement The prepared statement. + */ + public function prepare($statement) + { + $this->connect(); + + return new Statement($statement, $this); + } + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parameterized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters to bind to the query, if any. + * @return Doctrine\DBAL\Driver\Statement The executed statement. + * @internal PERF: Directly prepares a driver statement, not a wrapper. + */ + public function executeQuery($query, array $params = array(), $types = array()) + { + $this->connect(); + + $hasLogger = $this->_config->getSQLLogger() !== null; + if ($hasLogger) { + $this->_config->getSQLLogger()->startQuery($query, $params, $types); + } + + if ($params) { + $stmt = $this->_conn->prepare($query); + if ($types) { + $this->_bindTypedValues($stmt, $params, $types); + $stmt->execute(); + } else { + $stmt->execute($params); + } + } else { + $stmt = $this->_conn->query($query); + } + + if ($hasLogger) { + $this->_config->getSQLLogger()->stopQuery(); + } + + return $stmt; + } + + /** + * Executes an, optionally parameterized, SQL query and returns the result, + * applying a given projection/transformation function on each row of the result. + * + * @param string $query The SQL query to execute. + * @param array $params The parameters, if any. + * @param Closure $mapper The transformation function that is applied on each row. + * The function receives a single paramater, an array, that + * represents a row of the result set. + * @return mixed The projected result of the query. + */ + public function project($query, array $params, Closure $function) + { + $result = array(); + $stmt = $this->executeQuery($query, $params ?: array()); + + while ($row = $stmt->fetch()) { + $result[] = $function($row); + } + + $stmt->closeCursor(); + + return $result; + } + + /** + * Executes an SQL statement, returning a result set as a Statement object. + * + * @param string $statement + * @param integer $fetchType + * @return Doctrine\DBAL\Driver\Statement + */ + public function query() + { + $this->connect(); + + return call_user_func_array(array($this->_conn, 'query'), func_get_args()); + } + + /** + * Executes an SQL INSERT/UPDATE/DELETE query with the given parameters + * and returns the number of affected rows. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param string $query The SQL query. + * @param array $params The query parameters. + * @param array $types The parameter types. + * @return integer The number of affected rows. + * @internal PERF: Directly prepares a driver statement, not a wrapper. + */ + public function executeUpdate($query, array $params = array(), array $types = array()) + { + $this->connect(); + + $hasLogger = $this->_config->getSQLLogger() !== null; + if ($hasLogger) { + $this->_config->getSQLLogger()->startQuery($query, $params, $types); + } + + if ($params) { + $stmt = $this->_conn->prepare($query); + if ($types) { + $this->_bindTypedValues($stmt, $params, $types); + $stmt->execute(); + } else { + $stmt->execute($params); + } + $result = $stmt->rowCount(); + } else { + $result = $this->_conn->exec($query); + } + + if ($hasLogger) { + $this->_config->getSQLLogger()->stopQuery(); + } + + return $result; + } + + /** + * Execute an SQL statement and return the number of affected rows. + * + * @param string $statement + * @return integer The number of affected rows. + */ + public function exec($statement) + { + $this->connect(); + return $this->_conn->exec($statement); + } + + /** + * Returns the current transaction nesting level. + * + * @return integer The nesting level. A value of 0 means there's no active transaction. + */ + public function getTransactionNestingLevel() + { + return $this->_transactionNestingLevel; + } + + /** + * Fetch the SQLSTATE associated with the last database operation. + * + * @return integer The last error code. + */ + public function errorCode() + { + $this->connect(); + return $this->_conn->errorCode(); + } + + /** + * Fetch extended error information associated with the last database operation. + * + * @return array The last error information. + */ + public function errorInfo() + { + $this->connect(); + return $this->_conn->errorInfo(); + } + + /** + * Returns the ID of the last inserted row, or the last value from a sequence object, + * depending on the underlying driver. + * + * Note: This method may not return a meaningful or consistent result across different drivers, + * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY + * columns or sequences. + * + * @param string $seqName Name of the sequence object from which the ID should be returned. + * @return string A string representation of the last inserted ID. + */ + public function lastInsertId($seqName = null) + { + $this->connect(); + return $this->_conn->lastInsertId($seqName); + } + + /** + * Executes a function in a transaction. + * + * The function gets passed this Connection instance as an (optional) parameter. + * + * If an exception occurs during execution of the function or transaction commit, + * the transaction is rolled back and the exception re-thrown. + * + * @param Closure $func The function to execute transactionally. + */ + public function transactional(Closure $func) + { + $this->beginTransaction(); + try { + $func($this); + $this->commit(); + } catch (Exception $e) { + $this->rollback(); + throw $e; + } + } + + /** + * Set if nested transactions should use savepoints + * + * @param boolean + * @return void + */ + public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints) + { + if ($this->_transactionNestingLevel > 0) { + throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction(); + } + + if (!$this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_nestTransactionsWithSavepoints = $nestTransactionsWithSavepoints; + } + + /** + * Get if nested transactions should use savepoints + * + * @return boolean + */ + public function getNestTransactionsWithSavepoints() + { + return $this->_nestTransactionsWithSavepoints; + } + + /** + * Returns the savepoint name to use for nested transactions are false if they are not supported + * "savepointFormat" parameter is not set + * + * @return mixed a string with the savepoint name or false + */ + protected function _getNestedTransactionSavePointName() + { + return 'DOCTRINE2_SAVEPOINT_'.$this->_transactionNestingLevel; + } + + /** + * Starts a transaction by suspending auto-commit mode. + * + * @return void + */ + public function beginTransaction() + { + $this->connect(); + + ++$this->_transactionNestingLevel; + + if ($this->_transactionNestingLevel == 1) { + $this->_conn->beginTransaction(); + } else if ($this->_nestTransactionsWithSavepoints) { + $this->createSavepoint($this->_getNestedTransactionSavePointName()); + } + } + + /** + * Commits the current transaction. + * + * @return void + * @throws ConnectionException If the commit failed due to no active transaction or + * because the transaction was marked for rollback only. + */ + public function commit() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + if ($this->_isRollbackOnly) { + throw ConnectionException::commitFailedRollbackOnly(); + } + + $this->connect(); + + if ($this->_transactionNestingLevel == 1) { + $this->_conn->commit(); + } else if ($this->_nestTransactionsWithSavepoints) { + $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); + } + + --$this->_transactionNestingLevel; + } + + /** + * Cancel any database changes done during the current transaction. + * + * this method can be listened with onPreTransactionRollback and onTransactionRollback + * eventlistener methods + * + * @throws ConnectionException If the rollback operation failed. + */ + public function rollback() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + + $this->connect(); + + if ($this->_transactionNestingLevel == 1) { + $this->_transactionNestingLevel = 0; + $this->_conn->rollback(); + $this->_isRollbackOnly = false; + } else if ($this->_nestTransactionsWithSavepoints) { + $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); + --$this->_transactionNestingLevel; + } else { + $this->_isRollbackOnly = true; + --$this->_transactionNestingLevel; + } + } + + /** + * createSavepoint + * creates a new savepoint + * + * @param string $savepoint name of a savepoint to set + * @return void + */ + public function createSavepoint($savepoint) + { + if (!$this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_conn->exec($this->_platform->createSavePoint($savepoint)); + } + + /** + * releaseSavePoint + * releases given savepoint + * + * @param string $savepoint name of a savepoint to release + * @return void + */ + public function releaseSavepoint($savepoint) + { + if (!$this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + if ($this->_platform->supportsReleaseSavepoints()) { + $this->_conn->exec($this->_platform->releaseSavePoint($savepoint)); + } + } + + /** + * rollbackSavePoint + * releases given savepoint + * + * @param string $savepoint name of a savepoint to rollback to + * @return void + */ + public function rollbackSavepoint($savepoint) + { + if (!$this->_platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->_conn->exec($this->_platform->rollbackSavePoint($savepoint)); + } + + /** + * Gets the wrapped driver connection. + * + * @return Doctrine\DBAL\Driver\Connection + */ + public function getWrappedConnection() + { + $this->connect(); + + return $this->_conn; + } + + /** + * Gets the SchemaManager that can be used to inspect or change the + * database schema through the connection. + * + * @return Doctrine\DBAL\Schema\SchemaManager + */ + public function getSchemaManager() + { + if ( ! $this->_schemaManager) { + $this->_schemaManager = $this->_driver->getSchemaManager($this); + } + + return $this->_schemaManager; + } + + /** + * Marks the current transaction so that the only possible + * outcome for the transaction to be rolled back. + * + * @throws ConnectionException If no transaction is active. + */ + public function setRollbackOnly() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + $this->_isRollbackOnly = true; + } + + /** + * Check whether the current transaction is marked for rollback only. + * + * @return boolean + * @throws ConnectionException If no transaction is active. + */ + public function isRollbackOnly() + { + if ($this->_transactionNestingLevel == 0) { + throw ConnectionException::noActiveTransaction(); + } + return $this->_isRollbackOnly; + } + + /** + * Converts a given value to its database representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * @return mixed The converted value. + */ + public function convertToDatabaseValue($value, $type) + { + return Type::getType($type)->convertToDatabaseValue($value, $this->_platform); + } + + /** + * Converts a given value to its PHP representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * @return mixed The converted type. + */ + public function convertToPHPValue($value, $type) + { + return Type::getType($type)->convertToPHPValue($value, $this->_platform); + } + + /** + * Binds a set of parameters, some or all of which are typed with a PDO binding type + * or DBAL mapping type, to a given statement. + * + * @param $stmt The statement to bind the values to. + * @param array $params The map/list of named/positional parameters. + * @param array $types The parameter types (PDO binding types or DBAL mapping types). + * @internal Duck-typing used on the $stmt parameter to support driver statements as well as + * raw PDOStatement instances. + */ + private function _bindTypedValues($stmt, array $params, array $types) + { + // Check whether parameters are positional or named. Mixing is not allowed, just like in PDO. + if (is_int(key($params))) { + // Positional parameters + $typeOffset = isset($types[0]) ? -1 : 0; + $bindIndex = 1; + foreach ($params as $position => $value) { + $typeIndex = $bindIndex + $typeOffset; + if (isset($types[$typeIndex])) { + $type = $types[$typeIndex]; + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->_platform); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type; // PDO::PARAM_* constants + } + $stmt->bindValue($bindIndex, $value, $bindingType); + } else { + $stmt->bindValue($bindIndex, $value); + } + ++$bindIndex; + } + } else { + // Named parameters + foreach ($params as $name => $value) { + if (isset($types[$name])) { + $type = $types[$name]; + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->_platform); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type; // PDO::PARAM_* constants + } + $stmt->bindValue($name, $value, $bindingType); + } else { + $stmt->bindValue($name, $value); + } + } + } + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/ConnectionException.php b/Doctrine/DBAL/ConnectionException.php new file mode 100644 index 0000000..4aaeea6 --- /dev/null +++ b/Doctrine/DBAL/ConnectionException.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Doctrine\DBAL\ConnectionException + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 4628 $ + * @author Jonathan H. Wage . + */ + +namespace Doctrine\DBAL; + +/** + * Driver interface. + * Interface that all DBAL drivers must implement. + * + * @since 2.0 + */ +interface Driver +{ + /** + * Attempts to create a connection with the database. + * + * @param array $params All connection parameters passed by the user. + * @param string $username The username to use when connecting. + * @param string $password The password to use when connecting. + * @param array $driverOptions The driver options to use when connecting. + * @return Doctrine\DBAL\Driver\Connection The database connection. + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()); + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform(); + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param Doctrine\DBAL\Connection $conn + * @return Doctrine\DBAL\SchemaManager + */ + public function getSchemaManager(Connection $conn); + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName(); + + /** + * Get the name of the database connected to for this driver. + * + * @param Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(Connection $conn); +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/Connection.php b/Doctrine/DBAL/Driver/Connection.php new file mode 100644 index 0000000..4cc5776 --- /dev/null +++ b/Doctrine/DBAL/Driver/Connection.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +/** + * Connection interface. + * Driver connections must implement this interface. + * + * This resembles (a subset of) the PDO interface. + * + * @since 2.0 + */ +interface Connection +{ + function prepare($prepareString); + function query(); + function quote($input, $type=\PDO::PARAM_STR); + function exec($statement); + function lastInsertId($name = null); + function beginTransaction(); + function commit(); + function rollBack(); + function errorCode(); + function errorInfo(); +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php b/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php new file mode 100644 index 0000000..5d706de --- /dev/null +++ b/Doctrine/DBAL/Driver/IBMDB2/DB2Connection.php @@ -0,0 +1,115 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +class DB2Connection implements \Doctrine\DBAL\Driver\Connection +{ + private $_conn = null; + + public function __construct(array $params, $username, $password, $driverOptions = array()) + { + $isPersistant = (isset($params['persistent']) && $params['persistent'] == true); + + if ($isPersistant) { + $this->_conn = db2_pconnect($params['dbname'], $username, $password, $driverOptions); + } else { + $this->_conn = db2_connect($params['dbname'], $username, $password, $driverOptions); + } + if (!$this->_conn) { + throw new DB2Exception(db2_conn_errormsg()); + } + } + + function prepare($sql) + { + $stmt = @db2_prepare($this->_conn, $sql); + if (!$stmt) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return new DB2Statement($stmt); + } + + function query() + { + $args = func_get_args(); + $sql = $args[0]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + function quote($input, $type=\PDO::PARAM_STR) + { + $input = db2_escape_string($input); + if ($type == \PDO::PARAM_INT ) { + return $input; + } else { + return "'".$input."'"; + } + } + + function exec($statement) + { + $stmt = $this->prepare($statement); + $stmt->execute(); + return $stmt->rowCount(); + } + + function lastInsertId($name = null) + { + return db2_last_insert_id($this->_conn); + } + + function beginTransaction() + { + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_OFF); + } + + function commit() + { + if (!db2_commit($this->_conn)) { + throw new DB2Exception(db2_conn_errormsg($this->_conn)); + } + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_ON); + } + + function rollBack() + { + if (!db2_rollback($this->_conn)) { + throw new DB2Exception(db2_conn_errormsg($this->_conn)); + } + db2_autocommit($this->_conn, DB2_AUTOCOMMIT_ON); + } + + function errorCode() + { + return db2_conn_error($this->_conn); + } + + function errorInfo() + { + return array( + 0 => db2_conn_errormsg($this->_conn), + 1 => $this->errorCode(), + ); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php b/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php new file mode 100644 index 0000000..b32dcbd --- /dev/null +++ b/Doctrine/DBAL/Driver/IBMDB2/DB2Driver.php @@ -0,0 +1,108 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +use Doctrine\DBAL\Driver, + Doctrine\DBAL\Connection; + +/** + * IBM DB2 Driver + * + * @since 2.0 + * @author Benjamin Eberlei + */ +class DB2Driver implements Driver +{ + /** + * Attempts to create a connection with the database. + * + * @param array $params All connection parameters passed by the user. + * @param string $username The username to use when connecting. + * @param string $password The password to use when connecting. + * @param array $driverOptions The driver options to use when connecting. + * @return Doctrine\DBAL\Driver\Connection The database connection. + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + if ( !isset($params['schema']) ) { + + } + + if ($params['host'] !== 'localhost' && $params['host'] != '127.0.0.1') { + // if the host isn't localhost, use extended connection params + $params['dbname'] = 'DRIVER={IBM DB2 ODBC DRIVER}' . + ';DATABASE=' . $params['dbname'] . + ';HOSTNAME=' . $params['host'] . + ';PORT=' . $params['port'] . + ';PROTOCOL=' . $params['protocol'] . + ';UID=' . $username . + ';PWD=' . $password .';'; + $username = null; + $password = null; + } + + return new DB2Connection($params, $username, $password, $driverOptions); + } + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\DB2Platform; + } + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param Doctrine\DBAL\Connection $conn + * @return Doctrine\DBAL\SchemaManager + */ + public function getSchemaManager(Connection $conn) + { + return new \Doctrine\DBAL\Schema\DB2SchemaManager($conn); + } + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName() + { + return 'ibm_db2'; + } + + /** + * Get the name of the database connected to for this driver. + * + * @param Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} diff --git a/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php b/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php new file mode 100644 index 0000000..b2a8de6 --- /dev/null +++ b/Doctrine/DBAL/Driver/IBMDB2/DB2Exception.php @@ -0,0 +1,27 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +class DB2Exception extends \Exception +{ + +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php b/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php new file mode 100644 index 0000000..41bff92 --- /dev/null +++ b/Doctrine/DBAL/Driver/IBMDB2/DB2Statement.php @@ -0,0 +1,297 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\IBMDB2; + +class DB2Statement implements \Doctrine\DBAL\Driver\Statement +{ + private $_stmt = null; + + private $_bindParam = array(); + + /** + * DB2_BINARY, DB2_CHAR, DB2_DOUBLE, or DB2_LONG + * @var + */ + static private $_typeMap = array( + \PDO::PARAM_INT => DB2_LONG, + \PDO::PARAM_STR => DB2_CHAR, + ); + + public function __construct($stmt) + { + $this->_stmt = $stmt; + } + + /** + * Binds a value to a corresponding named or positional + * placeholder in the SQL statement that was used to prepare the statement. + * + * @param mixed $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $value The value to bind to the parameter. + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindValue($param, $value, $type = null) + { + return $this->bindParam($param, $value, $type); + } + + /** + * Binds a PHP variable to a corresponding named or question mark placeholder in the + * SQL statement that was use to prepare the statement. Unlike PDOStatement->bindValue(), + * the variable is bound as a reference and will only be evaluated at the time + * that PDOStatement->execute() is called. + * + * Most parameters are input parameters, that is, parameters that are + * used in a read-only fashion to build up the query. Some drivers support the invocation + * of stored procedures that return data as output parameters, and some also as input/output + * parameters that both send in data and are updated to receive it. + * + * @param mixed $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter. + * + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. To return + * an INOUT parameter from a stored procedure, use the bitwise OR operator to set the + * PDO::PARAM_INPUT_OUTPUT bits for the data_type parameter. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindParam($column, &$variable, $type = null) + { + $this->_bindParam[$column] =& $variable; + + if ($type && isset(self::$_typeMap[$type])) { + $type = self::$_typeMap[$type]; + } else { + $type = DB2_CHAR; + } + + if (!db2_bind_param($this->_stmt, $column, "variable", DB2_PARAM_IN, $type)) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return true; + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function closeCursor() + { + if (!$this->_stmt) { + return false; + } + + $this->_bindParam = array(); + db2_free_result($this->_stmt); + $ret = db2_free_stmt($this->_stmt); + $this->_stmt = false; + return $ret; + } + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + function columnCount() + { + if (!$this->_stmt) { + return false; + } + return db2_num_fields($this->_stmt); + } + + /** + * errorCode + * Fetch the SQLSTATE associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorCode() + * @return string error code string + */ + function errorCode() + { + return db2_stmt_error(); + } + + /** + * errorInfo + * Fetch extended error information associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorInfo() + * @return array error info array + */ + function errorInfo() + { + return array( + 0 => db2_stmt_errormsg(), + 1 => db2_stmt_error(), + ); + } + + /** + * Executes a prepared statement + * + * If the prepared statement included parameter markers, you must either: + * call PDOStatement->bindParam() to bind PHP variables to the parameter markers: + * bound variables pass their value as input and receive the output value, + * if any, of their associated parameter markers or pass an array of input-only + * parameter values + * + * + * @param array $params An array of values with as many elements as there are + * bound parameters in the SQL statement being executed. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function execute($params = null) + { + if (!$this->_stmt) { + return false; + } + + /*$retval = true; + if ($params !== null) { + $retval = @db2_execute($this->_stmt, $params); + } else { + $retval = @db2_execute($this->_stmt); + }*/ + if ($params === null) { + ksort($this->_bindParam); + $params = array_values($this->_bindParam); + } + $retval = @db2_execute($this->_stmt, $params); + + if ($retval === false) { + throw new DB2Exception(db2_stmt_errormsg()); + } + return $retval; + } + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $cursorOrientation For a PDOStatement object representing a scrollable cursor, + * this value determines which row will be returned to the caller. + * This value must be one of the Query::HYDRATE_ORI_* constants, defaulting to + * Query::HYDRATE_ORI_NEXT. To request a scrollable cursor for your + * PDOStatement object, + * you must set the PDO::ATTR_CURSOR attribute to Doctrine::CURSOR_SCROLL when you + * prepare the SQL statement with Doctrine_Adapter_Interface->prepare(). + * + * @param integer $cursorOffset For a PDOStatement object representing a scrollable cursor for which the + * $cursorOrientation parameter is set to Query::HYDRATE_ORI_ABS, this value specifies + * the absolute number of the row in the result set that shall be fetched. + * + * For a PDOStatement object representing a scrollable cursor for + * which the $cursorOrientation parameter is set to Query::HYDRATE_ORI_REL, this value + * specifies the row to fetch relative to the cursor position before + * PDOStatement->fetch() was called. + * + * @return mixed + */ + function fetch($fetchStyle = \PDO::FETCH_BOTH) + { + switch ($fetchStyle) { + case \PDO::FETCH_BOTH: + return db2_fetch_both($this->_stmt); + case \PDO::FETCH_ASSOC: + return db2_fetch_assoc($this->_stmt); + case \PDO::FETCH_NUM: + return db2_fetch_array($this->_stmt); + default: + throw new DB2Exception("Given Fetch-Style " . $fetchStyle . " is not supported."); + } + } + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $columnIndex Returns the indicated 0-indexed column when the value of $fetchStyle is + * Query::HYDRATE_COLUMN. Defaults to 0. + * + * @return array + */ + function fetchAll($fetchStyle = \PDO::FETCH_BOTH) + { + $rows = array(); + while ($row = $this->fetch($fetchStyle)) { + $rows[] = $row; + } + return $rows; + } + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(\PDO::FETCH_NUM); + if ($row && isset($row[$columnIndex])) { + return $row[$columnIndex]; + } + return false; + } + + /** + * rowCount + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * executed by the corresponding object. + * + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be + * relied on for portable applications. + * + * @return integer Returns the number of rows. + */ + function rowCount() + { + return (@db2_num_rows($this->_stmt))?:0; + } +} diff --git a/Doctrine/DBAL/Driver/OCI8/Driver.php b/Doctrine/DBAL/Driver/OCI8/Driver.php new file mode 100644 index 0000000..3d54669 --- /dev/null +++ b/Doctrine/DBAL/Driver/OCI8/Driver.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +use Doctrine\DBAL\Platforms; + +/** + * A Doctrine DBAL driver for the Oracle OCI8 PHP extensions. + * + * @author Roman Borschel + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new OCI8Connection( + $username, + $password, + $this->_constructDsn($params), + isset($params['charset']) ? $params['charset'] : null, + isset($params['sessionMode']) ? $params['sessionMode'] : OCI_DEFAULT + ); + } + + /** + * Constructs the Oracle DSN. + * + * @return string The DSN. + */ + private function _constructDsn(array $params) + { + $dsn = ''; + if (isset($params['host'])) { + $dsn .= '(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' . + '(HOST=' . $params['host'] . ')'; + + if (isset($params['port'])) { + $dsn .= '(PORT=' . $params['port'] . ')'; + } else { + $dsn .= '(PORT=1521)'; + } + + $dsn .= '))'; + if (isset($params['dbname'])) { + $dsn .= '(CONNECT_DATA=(SID=' . $params['dbname'] . ')'; + } + $dsn .= '))'; + } else { + $dsn .= $params['dbname']; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\OraclePlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\OracleSchemaManager($conn); + } + + public function getName() + { + return 'oci8'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['user']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php b/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php new file mode 100644 index 0000000..f65b40d --- /dev/null +++ b/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php @@ -0,0 +1,160 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +/** + * OCI8 implementation of the Connection interface. + * + * @since 2.0 + */ +class OCI8Connection implements \Doctrine\DBAL\Driver\Connection +{ + private $_dbh; + + private $_executeMode = OCI_COMMIT_ON_SUCCESS; + + /** + * Create a Connection to an Oracle Database using oci8 extension. + * + * @param string $username + * @param string $password + * @param string $db + */ + public function __construct($username, $password, $db, $charset = null, $sessionMode = OCI_DEFAULT) + { + if (!defined('OCI_NO_AUTO_COMMIT')) { + define('OCI_NO_AUTO_COMMIT', 0); + } + + $this->_dbh = @oci_connect($username, $password, $db, $charset, $sessionMode); + if (!$this->_dbh) { + throw OCI8Exception::fromErrorInfo(oci_error()); + } + } + + /** + * Create a non-executed prepared statement. + * + * @param string $prepareString + * @return OCI8Statement + */ + public function prepare($prepareString) + { + return new OCI8Statement($this->_dbh, $prepareString, $this->_executeMode); + } + + /** + * @param string $sql + * @return OCI8Statement + */ + public function query() + { + $args = func_get_args(); + $sql = $args[0]; + //$fetchMode = $args[1]; + $stmt = $this->prepare($sql); + $stmt->execute(); + return $stmt; + } + + /** + * Quote input value. + * + * @param mixed $input + * @param int $type PDO::PARAM* + * @return mixed + */ + public function quote($input, $type=\PDO::PARAM_STR) + { + return is_numeric($input) ? $input : "'$input'"; + } + + /** + * + * @param string $statement + * @return int + */ + public function exec($statement) + { + $stmt = $this->prepare($statement); + $stmt->execute(); + return $stmt->rowCount(); + } + + public function lastInsertId($name = null) + { + //TODO: throw exception or support sequences? + } + + /** + * Start a transactiom + * + * Oracle has to explicitly set the autocommit mode off. That means + * after connection, a commit or rollback there is always automatically + * opened a new transaction. + * + * @return bool + */ + public function beginTransaction() + { + $this->_executeMode = OCI_NO_AUTO_COMMIT; + return true; + } + + /** + * @throws OCI8Exception + * @return bool + */ + public function commit() + { + if (!oci_commit($this->_dbh)) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + $this->_executeMode = OCI_COMMIT_ON_SUCCESS; + return true; + } + + /** + * @throws OCI8Exception + * @return bool + */ + public function rollBack() + { + if (!oci_rollback($this->_dbh)) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + $this->_executeMode = OCI_COMMIT_ON_SUCCESS; + return true; + } + + public function errorCode() + { + $error = oci_error($this->_dbh); + if ($error !== false) { + $error = $error['code']; + } + return $error; + } + + public function errorInfo() + { + return oci_error($this->_dbh); + } +} diff --git a/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php b/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php new file mode 100644 index 0000000..66fe615 --- /dev/null +++ b/Doctrine/DBAL/Driver/OCI8/OCI8Exception.php @@ -0,0 +1,30 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\OCI8; + +class OCI8Exception extends \Exception +{ + static public function fromErrorInfo($error) + { + return new self($error['message'], $error['code']); + } +} diff --git a/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php b/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php new file mode 100644 index 0000000..60ccc10 --- /dev/null +++ b/Doctrine/DBAL/Driver/OCI8/OCI8Statement.php @@ -0,0 +1,220 @@ +. + */ + +namespace Doctrine\DBAL\Driver\OCI8; + +use \PDO; + +/** + * The OCI8 implementation of the Statement interface. + * + * @since 2.0 + * @author Roman Borschel + */ +class OCI8Statement implements \Doctrine\DBAL\Driver\Statement +{ + /** Statement handle. */ + private $_sth; + private $_executeMode; + private static $_PARAM = ':param'; + private static $fetchStyleMap = array( + PDO::FETCH_BOTH => OCI_BOTH, + PDO::FETCH_ASSOC => OCI_ASSOC, + PDO::FETCH_NUM => OCI_NUM + ); + private $_paramMap = array(); + + /** + * Creates a new OCI8Statement that uses the given connection handle and SQL statement. + * + * @param resource $dbh The connection handle. + * @param string $statement The SQL statement. + */ + public function __construct($dbh, $statement, $executeMode) + { + list($statement, $paramMap) = self::convertPositionalToNamedPlaceholders($statement); + $this->_sth = oci_parse($dbh, $statement); + $this->_paramMap = $paramMap; + $this->_executeMode = $executeMode; + } + + /** + * Convert positional (?) into named placeholders (:param) + * + * Oracle does not support positional parameters, hence this method converts all + * positional parameters into artificially named parameters. Note that this conversion + * is not perfect. All question marks (?) in the original statement are treated as + * placeholders and converted to a named parameter. + * + * The algorithm uses a state machine with two possible states: InLiteral and NotInLiteral. + * Question marks inside literal strings are therefore handled correctly by this method. + * This comes at a cost, the whole sql statement has to be looped over. + * + * @todo extract into utility class in Doctrine\DBAL\Util namespace + * @todo review and test for lost spaces. we experienced missing spaces with oci8 in some sql statements. + * @param string $statement The SQL statement to convert. + * @return string + */ + static public function convertPositionalToNamedPlaceholders($statement) + { + $count = 1; + $inLiteral = false; // a valid query never starts with quotes + $stmtLen = strlen($statement); + $paramMap = array(); + for ($i = 0; $i < $stmtLen; $i++) { + if ($statement[$i] == '?' && !$inLiteral) { + // real positional parameter detected + $paramMap[$count] = ":param$count"; + $len = strlen($paramMap[$count]); + $statement = substr_replace($statement, ":param$count", $i, 1); + $i += $len-1; // jump ahead + $stmtLen = strlen($statement); // adjust statement length + ++$count; + } else if ($statement[$i] == "'" || $statement[$i] == '"') { + $inLiteral = ! $inLiteral; // switch state! + } + } + + return array($statement, $paramMap); + } + + /** + * {@inheritdoc} + */ + public function bindValue($param, $value, $type = null) + { + return $this->bindParam($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function bindParam($column, &$variable, $type = null) + { + $column = isset($this->_paramMap[$column]) ? $this->_paramMap[$column] : $column; + + return oci_bind_by_name($this->_sth, $column, $variable); + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + public function closeCursor() + { + return oci_free_statement($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function columnCount() + { + return oci_num_fields($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function errorCode() + { + $error = oci_error($this->_sth); + if ($error !== false) { + $error = $error['code']; + } + return $error; + } + + /** + * {@inheritdoc} + */ + public function errorInfo() + { + return oci_error($this->_sth); + } + + /** + * {@inheritdoc} + */ + public function execute($params = null) + { + if ($params) { + $hasZeroIndex = isset($params[0]); + foreach ($params as $key => $val) { + if ($hasZeroIndex && is_numeric($key)) { + $this->bindValue($key + 1, $val); + } else { + $this->bindValue($key, $val); + } + } + } + + $ret = @oci_execute($this->_sth, $this->_executeMode); + if ( ! $ret) { + throw OCI8Exception::fromErrorInfo($this->errorInfo()); + } + return $ret; + } + + /** + * {@inheritdoc} + */ + public function fetch($fetchStyle = PDO::FETCH_BOTH) + { + if ( ! isset(self::$fetchStyleMap[$fetchStyle])) { + throw new \InvalidArgumentException("Invalid fetch style: " . $fetchStyle); + } + + return oci_fetch_array($this->_sth, self::$fetchStyleMap[$fetchStyle] | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + } + + /** + * {@inheritdoc} + */ + public function fetchAll($fetchStyle = PDO::FETCH_BOTH) + { + if ( ! isset(self::$fetchStyleMap[$fetchStyle])) { + throw new \InvalidArgumentException("Invalid fetch style: " . $fetchStyle); + } + + $result = array(); + oci_fetch_all($this->_sth, $result, 0, -1, + self::$fetchStyleMap[$fetchStyle] | OCI_RETURN_NULLS | OCI_FETCHSTATEMENT_BY_ROW | OCI_RETURN_LOBS); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function fetchColumn($columnIndex = 0) + { + $row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + return $row[$columnIndex]; + } + + /** + * {@inheritdoc} + */ + public function rowCount() + { + return oci_num_rows($this->_sth); + } +} diff --git a/Doctrine/DBAL/Driver/PDOConnection.php b/Doctrine/DBAL/Driver/PDOConnection.php new file mode 100644 index 0000000..f006807 --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOConnection.php @@ -0,0 +1,40 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use \PDO; + +/** + * PDO implementation of the Connection interface. + * Used by all PDO-based drivers. + * + * @since 2.0 + */ +class PDOConnection extends PDO implements Connection +{ + public function __construct($dsn, $user = null, $password = null, array $options = null) + { + parent::__construct($dsn, $user, $password, $options); + $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Doctrine\DBAL\Driver\PDOStatement', array())); + $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOIbm/Driver.php b/Doctrine/DBAL/Driver/PDOIbm/Driver.php new file mode 100644 index 0000000..844f2ab --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOIbm/Driver.php @@ -0,0 +1,126 @@ +. +*/ + +namespace Doctrine\DBAL\Driver\PDOIbm; + +use Doctrine\DBAL\Connection; + +/** + * Driver for the PDO IBM extension + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * Attempts to establish a connection with the underlying driver. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return Doctrine\DBAL\Driver\Connection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + $conn = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + return $conn; + } + + /** + * Constructs the MySql PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'ibm:'; + if (isset($params['host'])) { + $dsn .= 'HOSTNAME=' . $params['host'] . ';'; + } + if (isset($params['port'])) { + $dsn .= 'PORT=' . $params['port'] . ';'; + } + $dsn .= 'PROTOCOL=TCPIP;'; + if (isset($params['dbname'])) { + $dsn .= 'DATABASE=' . $params['dbname'] . ';'; + } + + return $dsn; + } + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return Doctrine\DBAL\Platforms\AbstractPlatform The database platform. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\DB2Platform; + } + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @param Doctrine\DBAL\Connection $conn + * @return Doctrine\DBAL\SchemaManager + */ + public function getSchemaManager(Connection $conn) + { + return new \Doctrine\DBAL\Schema\DB2SchemaManager($conn); + } + + /** + * Gets the name of the driver. + * + * @return string The name of the driver. + */ + public function getName() + { + return 'pdo_ibm'; + } + + /** + * Get the name of the database connected to for this driver. + * + * @param Doctrine\DBAL\Connection $conn + * @return string $database + */ + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOMySql/Driver.php b/Doctrine/DBAL/Driver/PDOMySql/Driver.php new file mode 100644 index 0000000..71a7f9f --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOMySql/Driver.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOMySql; + +use Doctrine\DBAL\Connection; + +/** + * PDO MySql driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * Attempts to establish a connection with the underlying driver. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return Doctrine\DBAL\Driver\Connection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + $conn = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + return $conn; + } + + /** + * Constructs the MySql PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'mysql:'; + if (isset($params['host'])) { + $dsn .= 'host=' . $params['host'] . ';'; + } + if (isset($params['port'])) { + $dsn .= 'port=' . $params['port'] . ';'; + } + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ';'; + } + if (isset($params['unix_socket'])) { + $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\MySqlPlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\MySqlSchemaManager($conn); + } + + public function getName() + { + return 'pdo_mysql'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOOracle/Driver.php b/Doctrine/DBAL/Driver/PDOOracle/Driver.php new file mode 100644 index 0000000..61102eb --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOOracle/Driver.php @@ -0,0 +1,88 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOOracle; + +use Doctrine\DBAL\Platforms; + +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Oracle PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'oci:'; + if (isset($params['host'])) { + $dsn .= 'dbname=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)' . + '(HOST=' . $params['host'] . ')'; + + if (isset($params['port'])) { + $dsn .= '(PORT=' . $params['port'] . ')'; + } else { + $dsn .= '(PORT=1521)'; + } + + $dsn .= '))(CONNECT_DATA=(SID=' . $params['dbname'] . ')))'; + } else { + $dsn .= 'dbname=' . $params['dbname']; + } + + if (isset($params['charset'])) { + $dsn .= ';charset=' . $params['charset']; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\OraclePlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\OracleSchemaManager($conn); + } + + public function getName() + { + return 'pdo_oracle'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['user']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOPgSql/Driver.php b/Doctrine/DBAL/Driver/PDOPgSql/Driver.php new file mode 100644 index 0000000..06c2a89 --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOPgSql/Driver.php @@ -0,0 +1,70 @@ +_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Postgres PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'pgsql:'; + if (isset($params['host'])) { + $dsn .= 'host=' . $params['host'] . ' '; + } + if (isset($params['port'])) { + $dsn .= 'port=' . $params['port'] . ' '; + } + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ' '; + } + + return $dsn; + } + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\PostgreSqlPlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\PostgreSqlSchemaManager($conn); + } + + public function getName() + { + return 'pdo_pgsql'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOSqlite/Driver.php b/Doctrine/DBAL/Driver/PDOSqlite/Driver.php new file mode 100644 index 0000000..1721d5d --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOSqlite/Driver.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlite; + +/** + * The PDO Sqlite driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + /** + * @var array + */ + protected $_userDefinedFunctions = array( + 'sqrt' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfSqrt'), 'numArgs' => 1), + 'mod' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfMod'), 'numArgs' => 2), + 'locate' => array('callback' => array('Doctrine\DBAL\Platforms\SqlitePlatform', 'udfLocate'), 'numArgs' => -1), + ); + + /** + * Tries to establish a database connection to SQLite. + * + * @param array $params + * @param string $username + * @param string $password + * @param array $driverOptions + * @return Connection + */ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + if (isset($driverOptions['userDefinedFunctions'])) { + $this->_userDefinedFunctions = array_merge( + $this->_userDefinedFunctions, $driverOptions['userDefinedFunctions']); + unset($driverOptions['userDefinedFunctions']); + } + + $pdo = new \Doctrine\DBAL\Driver\PDOConnection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + + foreach ($this->_userDefinedFunctions AS $fn => $data) { + $pdo->sqliteCreateFunction($fn, $data['callback'], $data['numArgs']); + } + + return $pdo; + } + + /** + * Constructs the Sqlite PDO DSN. + * + * @return string The DSN. + * @override + */ + protected function _constructPdoDsn(array $params) + { + $dsn = 'sqlite:'; + if (isset($params['path'])) { + $dsn .= $params['path']; + } else if (isset($params['memory'])) { + $dsn .= ':memory:'; + } + + return $dsn; + } + + /** + * Gets the database platform that is relevant for this driver. + */ + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\SqlitePlatform(); + } + + /** + * Gets the schema manager that is relevant for this driver. + * + * @param Doctrine\DBAL\Connection $conn + * @return Doctrine\DBAL\Schema\SqliteSchemaManager + */ + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\SqliteSchemaManager($conn); + } + + public function getName() + { + return 'pdo_sqlite'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return isset($params['path']) ? $params['path'] : null; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php b/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php new file mode 100644 index 0000000..f459004 --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOSqlsrv/Connection.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlsrv; + +/** + * Sqlsrv Connection implementation. + * + * @since 2.0 + */ +class Connection extends \Doctrine\DBAL\Driver\PDOConnection implements \Doctrine\DBAL\Driver\Connection +{ + /** + * @override + */ + public function quote($value, $type=\PDO::PARAM_STR) + { + $val = parent::quote($value, $type); + + // Fix for a driver version terminating all values with null byte + if (strpos($val, "\0") !== false) { + $val = substr($val, 0, -1); + } + + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php b/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php new file mode 100644 index 0000000..3c71ce0 --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOSqlsrv/Driver.php @@ -0,0 +1,86 @@ +. + */ + +namespace Doctrine\DBAL\Driver\PDOSqlsrv; + +/** + * The PDO-based Sqlsrv driver. + * + * @since 2.0 + */ +class Driver implements \Doctrine\DBAL\Driver +{ + public function connect(array $params, $username = null, $password = null, array $driverOptions = array()) + { + return new Connection( + $this->_constructPdoDsn($params), + $username, + $password, + $driverOptions + ); + } + + /** + * Constructs the Sqlsrv PDO DSN. + * + * @return string The DSN. + */ + private function _constructPdoDsn(array $params) + { + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + } + + if (isset($params['port']) && !empty($params['port'])) { + $dsn .= ',' . $params['port']; + } + + if (isset($params['dbname'])) { + $dsn .= ';Database=' . $params['dbname']; + } + + return $dsn; + } + + + public function getDatabasePlatform() + { + return new \Doctrine\DBAL\Platforms\MsSqlPlatform(); + } + + public function getSchemaManager(\Doctrine\DBAL\Connection $conn) + { + return new \Doctrine\DBAL\Schema\MsSqlSchemaManager($conn); + } + + public function getName() + { + return 'pdo_sqlsrv'; + } + + public function getDatabase(\Doctrine\DBAL\Connection $conn) + { + $params = $conn->getParams(); + return $params['dbname']; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/PDOStatement.php b/Doctrine/DBAL/Driver/PDOStatement.php new file mode 100644 index 0000000..50b1e21 --- /dev/null +++ b/Doctrine/DBAL/Driver/PDOStatement.php @@ -0,0 +1,33 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +/** + * The PDO implementation of the Statement interface. + * Used by all PDO-based drivers. + * + * @since 2.0 + */ +class PDOStatement extends \PDOStatement implements Statement +{ + private function __construct() {} +} \ No newline at end of file diff --git a/Doctrine/DBAL/Driver/Statement.php b/Doctrine/DBAL/Driver/Statement.php new file mode 100644 index 0000000..6cb8b64 --- /dev/null +++ b/Doctrine/DBAL/Driver/Statement.php @@ -0,0 +1,200 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use \PDO; + +/** + * Statement interface. + * Drivers must implement this interface. + * + * This resembles (a subset of) the PDOStatement interface. + * + * @author Konsta Vesterinen + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + */ +interface Statement +{ + /** + * Binds a value to a corresponding named or positional + * placeholder in the SQL statement that was used to prepare the statement. + * + * @param mixed $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $value The value to bind to the parameter. + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindValue($param, $value, $type = null); + + /** + * Binds a PHP variable to a corresponding named or question mark placeholder in the + * SQL statement that was use to prepare the statement. Unlike PDOStatement->bindValue(), + * the variable is bound as a reference and will only be evaluated at the time + * that PDOStatement->execute() is called. + * + * Most parameters are input parameters, that is, parameters that are + * used in a read-only fashion to build up the query. Some drivers support the invocation + * of stored procedures that return data as output parameters, and some also as input/output + * parameters that both send in data and are updated to receive it. + * + * @param mixed $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement + * using question mark placeholders, this will be the 1-indexed position of the parameter + * + * @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter. + * + * @param integer $type Explicit data type for the parameter using the PDO::PARAM_* constants. To return + * an INOUT parameter from a stored procedure, use the bitwise OR operator to set the + * PDO::PARAM_INPUT_OUTPUT bits for the data_type parameter. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function bindParam($column, &$variable, $type = null); + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function closeCursor(); + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + function columnCount(); + + /** + * errorCode + * Fetch the SQLSTATE associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorCode() + * @return string error code string + */ + function errorCode(); + + /** + * errorInfo + * Fetch extended error information associated with the last operation on the statement handle + * + * @see Doctrine_Adapter_Interface::errorInfo() + * @return array error info array + */ + function errorInfo(); + + /** + * Executes a prepared statement + * + * If the prepared statement included parameter markers, you must either: + * call PDOStatement->bindParam() to bind PHP variables to the parameter markers: + * bound variables pass their value as input and receive the output value, + * if any, of their associated parameter markers or pass an array of input-only + * parameter values + * + * + * @param array $params An array of values with as many elements as there are + * bound parameters in the SQL statement being executed. + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function execute($params = null); + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $cursorOrientation For a PDOStatement object representing a scrollable cursor, + * this value determines which row will be returned to the caller. + * This value must be one of the Query::HYDRATE_ORI_* constants, defaulting to + * Query::HYDRATE_ORI_NEXT. To request a scrollable cursor for your + * PDOStatement object, + * you must set the PDO::ATTR_CURSOR attribute to Doctrine::CURSOR_SCROLL when you + * prepare the SQL statement with Doctrine_Adapter_Interface->prepare(). + * + * @param integer $cursorOffset For a PDOStatement object representing a scrollable cursor for which the + * $cursorOrientation parameter is set to Query::HYDRATE_ORI_ABS, this value specifies + * the absolute number of the row in the result set that shall be fetched. + * + * For a PDOStatement object representing a scrollable cursor for + * which the $cursorOrientation parameter is set to Query::HYDRATE_ORI_REL, this value + * specifies the row to fetch relative to the cursor position before + * PDOStatement->fetch() was called. + * + * @return mixed + */ + function fetch($fetchStyle = PDO::FETCH_BOTH); + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $columnIndex Returns the indicated 0-indexed column when the value of $fetchStyle is + * Query::HYDRATE_COLUMN. Defaults to 0. + * + * @return array + */ + function fetchAll($fetchStyle = PDO::FETCH_BOTH); + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + function fetchColumn($columnIndex = 0); + + /** + * rowCount + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * executed by the corresponding object. + * + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be + * relied on for portable applications. + * + * @return integer Returns the number of rows. + */ + function rowCount(); +} \ No newline at end of file diff --git a/Doctrine/DBAL/DriverManager.php b/Doctrine/DBAL/DriverManager.php new file mode 100644 index 0000000..b66c2f8 --- /dev/null +++ b/Doctrine/DBAL/DriverManager.php @@ -0,0 +1,161 @@ +. + */ + +namespace Doctrine\DBAL; + +use Doctrine\Common\EventManager; + +/** + * Factory for creating Doctrine\DBAL\Connection instances. + * + * @author Roman Borschel + * @since 2.0 + */ +final class DriverManager +{ + /** + * List of supported drivers and their mappings to the driver classes. + * + * @var array + * @todo REMOVE. Users should directly supply class names instead. + */ + private static $_driverMap = array( + 'pdo_mysql' => 'Doctrine\DBAL\Driver\PDOMySql\Driver', + 'pdo_sqlite' => 'Doctrine\DBAL\Driver\PDOSqlite\Driver', + 'pdo_pgsql' => 'Doctrine\DBAL\Driver\PDOPgSql\Driver', + 'pdo_oci' => 'Doctrine\DBAL\Driver\PDOOracle\Driver', + 'oci8' => 'Doctrine\DBAL\Driver\OCI8\Driver', + 'ibm_db2' => 'Doctrine\DBAL\Driver\IBMDB2\DB2Driver', + 'pdo_ibm' => 'Doctrine\DBAL\Driver\PDOIbm\Driver', + 'pdo_sqlsrv' => 'Doctrine\DBAL\Driver\PDOSqlsrv\Driver', + ); + + /** Private constructor. This class cannot be instantiated. */ + private function __construct() { } + + /** + * Creates a connection object based on the specified parameters. + * This method returns a Doctrine\DBAL\Connection which wraps the underlying + * driver connection. + * + * $params must contain at least one of the following. + * + * Either 'driver' with one of the following values: + * pdo_mysql + * pdo_sqlite + * pdo_pgsql + * pdo_oracle + * pdo_sqlsrv + * + * OR 'driverClass' that contains the full class name (with namespace) of the + * driver class to instantiate. + * + * Other (optional) parameters: + * + * user (string): + * The username to use when connecting. + * + * password (string): + * The password to use when connecting. + * + * driverOptions (array): + * Any additional driver-specific options for the driver. These are just passed + * through to the driver. + * + * pdo: + * You can pass an existing PDO instance through this parameter. The PDO + * instance will be wrapped in a Doctrine\DBAL\Connection. + * + * wrapperClass: + * You may specify a custom wrapper class through the 'wrapperClass' + * parameter but this class MUST inherit from Doctrine\DBAL\Connection. + * + * @param array $params The parameters. + * @param Doctrine\DBAL\Configuration The configuration to use. + * @param Doctrine\Common\EventManager The event manager to use. + * @return Doctrine\DBAL\Connection + */ + public static function getConnection( + array $params, + Configuration $config = null, + EventManager $eventManager = null) + { + // create default config and event manager, if not set + if ( ! $config) { + $config = new Configuration(); + } + if ( ! $eventManager) { + $eventManager = new EventManager(); + } + + // check for existing pdo object + if (isset($params['pdo']) && ! $params['pdo'] instanceof \PDO) { + throw DBALException::invalidPdoInstance(); + } else if (isset($params['pdo'])) { + $params['pdo']->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $params['driver'] = 'pdo_' . $params['pdo']->getAttribute(\PDO::ATTR_DRIVER_NAME); + } else { + self::_checkParams($params); + } + if (isset($params['driverClass'])) { + $className = $params['driverClass']; + } else { + $className = self::$_driverMap[$params['driver']]; + } + + $driver = new $className(); + + $wrapperClass = 'Doctrine\DBAL\Connection'; + if (isset($params['wrapperClass'])) { + if (is_subclass_of($params['wrapperClass'], $wrapperClass)) { + $wrapperClass = $params['wrapperClass']; + } else { + throw DBALException::invalidWrapperClass($params['wrapperClass']); + } + } + + return new $wrapperClass($params, $driver, $config, $eventManager); + } + + /** + * Checks the list of parameters. + * + * @param array $params + */ + private static function _checkParams(array $params) + { + // check existance of mandatory parameters + + // driver + if ( ! isset($params['driver']) && ! isset($params['driverClass'])) { + throw DBALException::driverRequired(); + } + + // check validity of parameters + + // driver + if ( isset($params['driver']) && ! isset(self::$_driverMap[$params['driver']])) { + throw DBALException::unknownDriver($params['driver'], array_keys(self::$_driverMap)); + } + + if (isset($params['driverClass']) && ! in_array('Doctrine\DBAL\Driver', class_implements($params['driverClass'], true))) { + throw DBALException::invalidDriverClass($params['driverClass']); + } + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Event/ConnectionEventArgs.php b/Doctrine/DBAL/Event/ConnectionEventArgs.php new file mode 100644 index 0000000..ce80ecd --- /dev/null +++ b/Doctrine/DBAL/Event/ConnectionEventArgs.php @@ -0,0 +1,79 @@ +. +*/ + +namespace Doctrine\DBAL\Event; + +use Doctrine\Common\EventArgs, + Doctrine\DBAL\Connection; + +/** + * Event Arguments used when a Driver connection is established inside Doctrine\DBAL\Connection. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ConnectionEventArgs extends EventArgs +{ + /** + * @var Connection + */ + private $_connection = null; + + public function __construct(Connection $connection) + { + $this->_connection = $connection; + } + + /** + * @return Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @return Doctrine\DBAL\Driver + */ + public function getDriver() + { + return $this->_connection->getDriver(); + } + + /** + * @return Doctrine\DBAL\Platforms\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_connection->getDatabasePlatform(); + } + + /** + * @return Doctrine\DBAL\Schema\AbstractSchemaManager + */ + public function getSchemaManager() + { + return $this->_connection->getSchemaManager(); + } +} diff --git a/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php b/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php new file mode 100644 index 0000000..9d0ff68 --- /dev/null +++ b/Doctrine/DBAL/Event/Listeners/MysqlSessionInit.php @@ -0,0 +1,75 @@ +. +*/ + +namespace Doctrine\DBAL\Event\Listeners; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +/** + * MySQL Session Init Event Subscriber which allows to set the Client Encoding of the Connection + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class MysqlSessionInit implements EventSubscriber +{ + /** + * @var string + */ + private $_charset; + + /** + * @var string + */ + private $_collation; + + /** + * Configure Charset and Collation options of MySQL Client for each Connection + * + * @param string $charset + * @param string $collation + */ + public function __construct($charset = 'utf8', $collation = false) + { + $this->_charset = $charset; + $this->_collation = $collation; + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) + { + $collation = ($this->_collation) ? " COLLATE ".$this->_collation : ""; + $args->getConnection()->executeUpdate("SET NAMES ".$this->_charset . $collation); + } + + public function getSubscribedEvents() + { + return array(Events::postConnect); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php b/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php new file mode 100644 index 0000000..3e5dd31 --- /dev/null +++ b/Doctrine/DBAL/Event/Listeners/OracleSessionInit.php @@ -0,0 +1,82 @@ +. +*/ + +namespace Doctrine\DBAL\Event\Listeners; + +use Doctrine\DBAL\Event\ConnectionEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\Common\EventSubscriber; + +/** + * Should be used when Oracle Server default enviroment does not match the Doctrine requirements. + * + * The following enviroment variables are required for the Doctrine default date format: + * + * NLS_TIME_FORMAT="HH24:MI:SS" + * NLS_DATE_FORMAT="YYYY-MM-DD" + * NLS_TIMESTAMP_FORMAT="YYYY-MM-DD HH24:MI:SS" + * NLS_TIMESTAMP_TZ_FORMAT="YYYY-MM-DD HH24:MI:SS TZH:TZM" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class OracleSessionInit implements EventSubscriber +{ + protected $_defaultSessionVars = array( + 'NLS_TIME_FORMAT' => "HH24:MI:SS", + 'NLS_DATE_FORMAT' => "YYYY-MM-DD HH24:MI:SS", + 'NLS_TIMESTAMP_FORMAT' => "YYYY-MM-DD HH24:MI:SS", + 'NLS_TIMESTAMP_TZ_FORMAT' => "YYYY-MM-DD HH24:MI:SS TZH:TZM", + ); + + /** + * @param array $oracleSessionVars + */ + public function __construct(array $oracleSessionVars = array()) + { + $this->_defaultSessionVars = array_merge($this->_defaultSessionVars, $oracleSessionVars); + } + + /** + * @param ConnectionEventArgs $args + * @return void + */ + public function postConnect(ConnectionEventArgs $args) + { + if (count($this->_defaultSessionVars)) { + array_change_key_case($this->_defaultSessionVars, \CASE_UPPER); + $vars = array(); + foreach ($this->_defaultSessionVars AS $option => $value) { + $vars[] = $option." = '".$value."'"; + } + $sql = "ALTER SESSION SET ".implode(" ", $vars); + $args->getConnection()->executeUpdate($sql); + } + } + + public function getSubscribedEvents() + { + return array(Events::postConnect); + } +} diff --git a/Doctrine/DBAL/Events.php b/Doctrine/DBAL/Events.php new file mode 100644 index 0000000..643789b --- /dev/null +++ b/Doctrine/DBAL/Events.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Container for all DBAL events. + * + * This class cannot be instantiated. + * + * @author Roman Borschel + * @since 2.0 + */ +final class Events +{ + private function __construct() {} + + const postConnect = 'postConnect'; +} + diff --git a/Doctrine/DBAL/LockMode.php b/Doctrine/DBAL/LockMode.php new file mode 100644 index 0000000..dff96de --- /dev/null +++ b/Doctrine/DBAL/LockMode.php @@ -0,0 +1,42 @@ +. +*/ + +namespace Doctrine\DBAL; + +/** + * Contains all DBAL LockModes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class LockMode +{ + const NONE = 0; + const OPTIMISTIC = 1; + const PESSIMISTIC_READ = 2; + const PESSIMISTIC_WRITE = 4; + + final private function __construct() { } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Logging/DebugStack.php b/Doctrine/DBAL/Logging/DebugStack.php new file mode 100644 index 0000000..ee4f640 --- /dev/null +++ b/Doctrine/DBAL/Logging/DebugStack.php @@ -0,0 +1,65 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * Includes executed SQLs in a Debug Stack + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DebugStack implements SQLLogger +{ + /** @var array $queries Executed SQL queries. */ + public $queries = array(); + + /** @var boolean $enabled If Debug Stack is enabled (log queries) or not. */ + public $enabled = true; + + public $start = null; + + /** + * {@inheritdoc} + */ + public function startQuery($sql, array $params = null, array $types = null) + { + if ($this->enabled) { + $this->start = microtime(true); + $this->queries[] = array('sql' => $sql, 'params' => $params, 'types' => $types, 'executionMS' => 0); + } + } + + /** + * {@inheritdoc} + */ + public function stopQuery() + { + $this->queries[(count($this->queries)-1)]['executionMS'] = microtime(true) - $this->start; + } +} + diff --git a/Doctrine/DBAL/Logging/EchoSQLLogger.php b/Doctrine/DBAL/Logging/EchoSQLLogger.php new file mode 100644 index 0000000..5545f25 --- /dev/null +++ b/Doctrine/DBAL/Logging/EchoSQLLogger.php @@ -0,0 +1,61 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * A SQL logger that logs to the standard output using echo/var_dump. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EchoSQLLogger implements SQLLogger +{ + /** + * {@inheritdoc} + */ + public function startQuery($sql, array $params = null, array $types = null) + { + echo $sql . PHP_EOL; + + if ($params) { + var_dump($params); + } + + if ($types) { + var_dump($types); + } + } + + /** + * {@inheritdoc} + */ + public function stopQuery() + { + + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Logging/SQLLogger.php b/Doctrine/DBAL/Logging/SQLLogger.php new file mode 100644 index 0000000..3b85868 --- /dev/null +++ b/Doctrine/DBAL/Logging/SQLLogger.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\DBAL\Logging; + +/** + * Interface for SQL loggers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +interface SQLLogger +{ + /** + * Logs a SQL statement somewhere. + * + * @param string $sql The SQL to be executed. + * @param array $params The SQL parameters. + * @param float $executionMS The microtime difference it took to execute this query. + * @return void + */ + public function startQuery($sql, array $params = null, array $types = null); + + /** + * Mark the last started query as stopped. This can be used for timing of queries. + * + * @return void + */ + public function stopQuery(); +} \ No newline at end of file diff --git a/Doctrine/DBAL/Platforms/AbstractPlatform.php b/Doctrine/DBAL/Platforms/AbstractPlatform.php new file mode 100644 index 0000000..6a47426 --- /dev/null +++ b/Doctrine/DBAL/Platforms/AbstractPlatform.php @@ -0,0 +1,2047 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Connection, + Doctrine\DBAL\Types, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Index, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\TableDiff; + +/** + * Base class for all DatabasePlatforms. The DatabasePlatforms are the central + * point of abstraction of platform-specific behaviors, features and SQL dialects. + * They are a passive source of information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @todo Remove any unnecessary methods. + */ +abstract class AbstractPlatform +{ + /** + * @var int + */ + const CREATE_INDEXES = 1; + + /** + * @var int + */ + const CREATE_FOREIGNKEYS = 2; + + /** + * @var int + */ + const TRIM_UNSPECIFIED = 0; + + /** + * @var int + */ + const TRIM_LEADING = 1; + + /** + * @var int + */ + const TRIM_TRAILING = 2; + + /** + * @var int + */ + const TRIM_BOTH = 3; + + /** + * @var array + */ + protected $doctrineTypeMapping = null; + + /** + * Constructor. + */ + public function __construct() {} + + /** + * Gets the SQL snippet that declares a boolean column. + * + * @param array $columnDef + * @return string + */ + abstract public function getBooleanTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares a 4 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getIntegerTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares an 8 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getBigIntTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares a 2 byte integer column. + * + * @param array $columnDef + * @return string + */ + abstract public function getSmallIntTypeDeclarationSQL(array $columnDef); + + /** + * Gets the SQL snippet that declares common properties of an integer column. + * + * @param array $columnDef + * @return string + */ + abstract protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef); + + /** + * Lazy load Doctrine Type Mappings + * + * @return void + */ + abstract protected function initializeDoctrineTypeMappings(); + + /** + * Gets the SQL snippet used to declare a VARCHAR column type. + * + * @param array $field + */ + abstract public function getVarcharTypeDeclarationSQL(array $field); + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * + * @param array $field + */ + abstract public function getClobTypeDeclarationSQL(array $field); + + /** + * Gets the name of the platform. + * + * @return string + */ + abstract public function getName(); + + /** + * Register a doctrine type to be used in conjunction with a column type of this platform. + * + * @param string $dbType + * @param string $doctrineType + */ + public function registerDoctrineTypeMapping($dbType, $doctrineType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeDoctrineTypeMappings(); + } + + if (!Types\Type::hasType($doctrineType)) { + throw DBALException::typeNotFound($doctrineType); + } + + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $doctrineType; + } + + /** + * Get the Doctrine type that is mapped for the given database column type. + * + * @param string $dbType + * @return string + */ + public function getDoctrineTypeMapping($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + if (isset($this->doctrineTypeMapping[$dbType])) { + return $this->doctrineTypeMapping[$dbType]; + } else { + throw new \Doctrine\DBAL\DBALException("Unknown database type ".$dbType." requested, " . get_class($this) . " may not support it."); + } + } + + /** + * Check if a database type is currently supported by this platform. + * + * @param string $dbType + * @return bool + */ + public function hasDoctrineTypeMappingFor($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + return isset($this->doctrineTypeMapping[$dbType]); + } + + /** + * Gets the character used for identifier quoting. + * + * @return string + */ + public function getIdentifierQuoteCharacter() + { + return '"'; + } + + /** + * Gets the string portion that starts an SQL comment. + * + * @return string + */ + public function getSqlCommentStartString() + { + return "--"; + } + + /** + * Gets the string portion that ends an SQL comment. + * + * @return string + */ + public function getSqlCommentEndString() + { + return "\n"; + } + + /** + * Gets the maximum length of a varchar field. + * + * @return integer + */ + public function getVarcharMaxLength() + { + return 4000; + } + + /** + * Gets the default length of a varchar field. + * + * @return integer + */ + public function getVarcharDefaultLength() + { + return 255; + } + + /** + * Gets all SQL wildcard characters of the platform. + * + * @return array + */ + public function getWildcards() + { + return array('%', '_'); + } + + /** + * Returns the regular expression operator. + * + * @return string + */ + public function getRegexpExpression() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns the average value of a column + * + * @param string $column the column to use + * @return string generated sql including an AVG aggregate function + */ + public function getAvgExpression($column) + { + return 'AVG(' . $column . ')'; + } + + /** + * Returns the number of rows (without a NULL value) of a column + * + * If a '*' is used instead of a column the number of selected rows + * is returned. + * + * @param string|integer $column the column to use + * @return string generated sql including a COUNT aggregate function + */ + public function getCountExpression($column) + { + return 'COUNT(' . $column . ')'; + } + + /** + * Returns the highest value of a column + * + * @param string $column the column to use + * @return string generated sql including a MAX aggregate function + */ + public function getMaxExpression($column) + { + return 'MAX(' . $column . ')'; + } + + /** + * Returns the lowest value of a column + * + * @param string $column the column to use + * @return string + */ + public function getMinExpression($column) + { + return 'MIN(' . $column . ')'; + } + + /** + * Returns the total sum of a column + * + * @param string $column the column to use + * @return string + */ + public function getSumExpression($column) + { + return 'SUM(' . $column . ')'; + } + + // scalar functions + + /** + * Returns the md5 sum of a field. + * + * Note: Not SQL92, but common functionality + * + * @return string + */ + public function getMd5Expression($column) + { + return 'MD5(' . $column . ')'; + } + + /** + * Returns the length of a text field. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getLengthExpression($column) + { + return 'LENGTH(' . $column . ')'; + } + + /** + * Rounds a numeric field to the number of decimals specified. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getRoundExpression($column, $decimals = 0) + { + return 'ROUND(' . $column . ', ' . $decimals . ')'; + } + + /** + * Returns the remainder of the division operation + * $expression1 / $expression2. + * + * @param string $expression1 + * @param string $expression2 + * @return string + */ + public function getModExpression($expression1, $expression2) + { + return 'MOD(' . $expression1 . ', ' . $expression2 . ')'; + } + + /** + * Trim a string, leading/trailing/both and with a given char which defaults to space. + * + * @param string $str + * @param int $pos + * @param string $char has to be quoted already + * @return string + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + $posStr = ''; + $trimChar = ($char != false) ? $char . ' FROM ' : ''; + + if ($pos == self::TRIM_LEADING) { + $posStr = 'LEADING '.$trimChar; + } else if($pos == self::TRIM_TRAILING) { + $posStr = 'TRAILING '.$trimChar; + } else if($pos == self::TRIM_BOTH) { + $posStr = 'BOTH '.$trimChar; + } + + return 'TRIM(' . $posStr . $str . ')'; + } + + /** + * rtrim + * returns the string $str with proceeding space characters removed + * + * @param string $str literal string or column name + * @return string + */ + public function getRtrimExpression($str) + { + return 'RTRIM(' . $str . ')'; + } + + /** + * ltrim + * returns the string $str with leading space characters removed + * + * @param string $str literal string or column name + * @return string + */ + public function getLtrimExpression($str) + { + return 'LTRIM(' . $str . ')'; + } + + /** + * upper + * Returns the string $str with all characters changed to + * uppercase according to the current character set mapping. + * + * @param string $str literal string or column name + * @return string + */ + public function getUpperExpression($str) + { + return 'UPPER(' . $str . ')'; + } + + /** + * lower + * Returns the string $str with all characters changed to + * lowercase according to the current character set mapping. + * + * @param string $str literal string or column name + * @return string + */ + public function getLowerExpression($str) + { + return 'LOWER(' . $str . ')'; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Returns the current system date. + * + * @return string + */ + public function getNowExpression() + { + return 'NOW()'; + } + + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function + * + * @param string $value an sql string literal or column name/alias + * @param integer $from where to start the substring portion + * @param integer $len the substring portion length + * @return string + */ + public function getSubstringExpression($value, $from, $len = null) + { + if ($len === null) + return 'SUBSTRING(' . $value . ' FROM ' . $from . ')'; + else { + return 'SUBSTRING(' . $value . ' FROM ' . $from . ' FOR ' . $len . ')'; + } + } + + /** + * Returns a series of strings concatinated + * + * concat() accepts an arbitrary number of parameters. Each parameter + * must contain an expression + * + * @param string $arg1, $arg2 ... $argN strings that will be concatinated. + * @return string + */ + public function getConcatExpression() + { + return join(' || ' , func_get_args()); + } + + /** + * Returns the SQL for a logical not. + * + * Example: + * + * $q = new Doctrine_Query(); + * $e = $q->expr; + * $q->select('*')->from('table') + * ->where($e->eq('id', $e->not('null')); + * + * + * @return string a logical expression + */ + public function getNotExpression($expression) + { + return 'NOT(' . $expression . ')'; + } + + /** + * Returns the SQL to check if a value is one in a set of + * given values. + * + * in() accepts an arbitrary number of parameters. The first parameter + * must always specify the value that should be matched against. Successive + * must contain a logical expression or an array with logical expressions. + * These expressions will be matched against the first parameter. + * + * @param string $column the value that should be matched against + * @param string|array(string) values that will be matched against $column + * @return string logical expression + */ + public function getInExpression($column, $values) + { + if ( ! is_array($values)) { + $values = array($values); + } + $values = $this->getIdentifiers($values); + + if (count($values) == 0) { + throw \InvalidArgumentException('Values must not be empty.'); + } + return $column . ' IN (' . implode(', ', $values) . ')'; + } + + /** + * Returns SQL that checks if a expression is null. + * + * @param string $expression the expression that should be compared to null + * @return string logical expression + */ + public function getIsNullExpression($expression) + { + return $expression . ' IS NULL'; + } + + /** + * Returns SQL that checks if a expression is not null. + * + * @param string $expression the expression that should be compared to null + * @return string logical expression + */ + public function getIsNotNullExpression($expression) + { + return $expression . ' IS NOT NULL'; + } + + /** + * Returns SQL that checks if an expression evaluates to a value between + * two values. + * + * The parameter $expression is checked if it is between $value1 and $value2. + * + * Note: There is a slight difference in the way BETWEEN works on some databases. + * http://www.w3schools.com/sql/sql_between.asp. If you want complete database + * independence you should avoid using between(). + * + * @param string $expression the value to compare to + * @param string $value1 the lower value to compare with + * @param string $value2 the higher value to compare with + * @return string logical expression + */ + public function getBetweenExpression($expression, $value1, $value2) + { + return $expression . ' BETWEEN ' .$value1 . ' AND ' . $value2; + } + + public function getAcosExpression($value) + { + return 'ACOS(' . $value . ')'; + } + + public function getSinExpression($value) + { + return 'SIN(' . $value . ')'; + } + + public function getPiExpression() + { + return 'PI()'; + } + + public function getCosExpression($value) + { + return 'COS(' . $value . ')'; + } + + public function getForUpdateSQL() + { + return 'FOR UPDATE'; + } + + /** + * Honors that some SQL vendors such as MsSql use table hints for locking instead of the ANSI SQL FOR UPDATE specification. + * + * @param string $fromClause + * @param int $lockMode + * @return string + */ + public function appendLockHint($fromClause, $lockMode) + { + return $fromClause; + } + + /** + * Get the sql snippet to append to any SELECT statement which locks rows in shared read lock. + * + * This defaults to the ASNI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database + * vendors allow to lighten this constraint up to be a real read lock. + * + * @return string + */ + public function getReadLockSQL() + { + return $this->getForUpdateSQL(); + } + + /** + * Get the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. + * + * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ASNI SQL standard. + * + * @return string + */ + public function getWriteLockSQL() + { + return $this->getForUpdateSQL(); + } + + public function getDropDatabaseSQL($database) + { + return 'DROP DATABASE ' . $database; + } + + /** + * Drop a Table + * + * @param Table|string $table + * @return string + */ + public function getDropTableSQL($table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return 'DROP TABLE ' . $table; + } + + /** + * Drop index from a table + * + * @param Index|string $name + * @param string|Table $table + * @return string + */ + public function getDropIndexSQL($index, $table=null) + { + if($index instanceof \Doctrine\DBAL\Schema\Index) { + $index = $index->getQuotedName($this); + } else if(!is_string($index)) { + throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + return 'DROP INDEX ' . $index; + } + + /** + * Get drop constraint sql + * + * @param \Doctrine\DBAL\Schema\Constraint $constraint + * @param string|Table $table + * @return string + */ + public function getDropConstraintSQL($constraint, $table) + { + if ($constraint instanceof \Doctrine\DBAL\Schema\Constraint) { + $constraint = $constraint->getQuotedName($this); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint; + } + + /** + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * @return string + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; + } + + /** + * Gets the SQL statement(s) to create a table with the specified name, columns and constraints + * on this platform. + * + * @param string $table The name of the table. + * @param int $createFlags + * @return array The sequence of SQL statements. + */ + public function getCreateTableSQL(Table $table, $createFlags=self::CREATE_INDEXES) + { + if ( ! is_int($createFlags)) { + throw new \InvalidArgumentException("Second argument of AbstractPlatform::getCreateTableSQL() has to be integer."); + } + + if (count($table->getColumns()) == 0) { + throw DBALException::noColumnsSpecifiedForTable($table->getName()); + } + + $tableName = $table->getQuotedName($this); + $options = $table->getOptions(); + $options['uniqueConstraints'] = array(); + $options['indexes'] = array(); + $options['primary'] = array(); + + if (($createFlags&self::CREATE_INDEXES) > 0) { + foreach ($table->getIndexes() AS $index) { + /* @var $index Index */ + if ($index->isPrimary()) { + $options['primary'] = $index->getColumns(); + } else { + $options['indexes'][$index->getName()] = $index; + } + } + } + + $columns = array(); + foreach ($table->getColumns() AS $column) { + /* @var \Doctrine\DBAL\Schema\Column $column */ + $columnData = array(); + $columnData['name'] = $column->getQuotedName($this); + $columnData['type'] = $column->getType(); + $columnData['length'] = $column->getLength(); + $columnData['notnull'] = $column->getNotNull(); + $columnData['unique'] = false; // TODO: what do we do about this? + $columnData['version'] = ($column->hasPlatformOption("version"))?$column->getPlatformOption('version'):false; + if(strtolower($columnData['type']) == "string" && $columnData['length'] === null) { + $columnData['length'] = 255; + } + $columnData['precision'] = $column->getPrecision(); + $columnData['scale'] = $column->getScale(); + $columnData['default'] = $column->getDefault(); + $columnData['columnDefinition'] = $column->getColumnDefinition(); + $columnData['autoincrement'] = $column->getAutoincrement(); + + if(in_array($column->getName(), $options['primary'])) { + $columnData['primary'] = true; + } + + $columns[$columnData['name']] = $columnData; + } + + if (($createFlags&self::CREATE_FOREIGNKEYS) > 0) { + $options['foreignKeys'] = array(); + foreach ($table->getForeignKeys() AS $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; + } + } + + return $this->_getCreateTableSQL($tableName, $columns, $options); + } + + /** + * @param string $tableName + * @param array $columns + * @param array $options + * @return array + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $columnListSql .= ', ' . $this->getIndexDeclarationSQL($index, $definition); + } + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if ( ! empty($check)) { + $query .= ', ' . $check; + } + $query .= ')'; + + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] AS $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE TEMPORARY TABLE"; + } + + /** + * Gets the SQL to create a sequence on this platform. + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @throws DBALException + */ + public function getCreateSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Gets the SQL to create a constraint on a table on this platform. + * + * @param Constraint $constraint + * @param string|Table $table + * @return string + */ + public function getCreateConstraintSQL(\Doctrine\DBAL\Schema\Constraint $constraint, $table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this); + + $columns = array(); + foreach ($constraint->getColumns() as $column) { + $columns[] = $column; + } + $columnList = '('. implode(', ', $columns) . ')'; + + $referencesClause = ''; + if ($constraint instanceof \Doctrine\DBAL\Schema\Index) { + if($constraint->isPrimary()) { + $query .= ' PRIMARY KEY'; + } elseif ($constraint->isUnique()) { + $query .= ' UNIQUE'; + } else { + throw new \InvalidArgumentException( + 'Can only create primary or unique constraints, no common indexes with getCreateConstraintSQL().' + ); + } + } else if ($constraint instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $query .= ' FOREIGN KEY'; + + $foreignColumns = array(); + foreach ($constraint->getForeignColumns() AS $column) { + $foreignColumns[] = $column; + } + + $referencesClause = ' REFERENCES '.$constraint->getForeignTableName(). ' ('.implode(', ', $foreignColumns).')'; + } + $query .= ' '.$columnList.$referencesClause; + + return $query; + } + + /** + * Gets the SQL to create an index on a table on this platform. + * + * @param Index $index + * @param string|Table $table name of the table on which the index is to be created + * @return string + */ + public function getCreateIndexSQL(Index $index, $table) + { + if ($table instanceof Table) { + $table = $table->getQuotedName($this); + } + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + $type = ''; + if ($index->isUnique()) { + $type = 'UNIQUE '; + } + + $query = 'CREATE ' . $type . 'INDEX ' . $name . ' ON ' . $table; + + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($columns) . ')'; + + return $query; + } + + /** + * Quotes a string so that it can be safely used as a table or column name, + * even if it is a reserved word of the platform. + * + * NOTE: Just because you CAN use quoted identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str identifier name to be quoted + * @return string quoted identifier string + */ + public function quoteIdentifier($str) + { + $c = $this->getIdentifierQuoteCharacter(); + + return $c . $str . $c; + } + + /** + * Create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey ForeignKey instance + * @param string|Table $table name of the table on which the foreign key is to be created + * @return string + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + $query = 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); + + return $query; + } + + /** + * Gets the sql statements for altering an existing table. + * + * The method returns an array of sql statements, since some platforms need several statements. + * + * @param TableDiff $diff + * @return array + */ + public function getAlterTableSQL(TableDiff $diff) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Common code for alter table statement generation that updates the changed Index and Foreign Key definitions. + * + * @param TableDiff $diff + * @return array + */ + protected function _getAlterTableIndexForeignKeySQL(TableDiff $diff) + { + if ($diff->newName !== false) { + $tableName = $diff->newName; + } else { + $tableName = $diff->name; + } + + $sql = array(); + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->removedForeignKeys AS $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName); + } + foreach ($diff->addedForeignKeys AS $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + foreach ($diff->changedForeignKeys AS $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableName); + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableName); + } + } + + foreach ($diff->addedIndexes AS $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + foreach ($diff->removedIndexes AS $index) { + $sql[] = $this->getDropIndexSQL($index, $tableName); + } + foreach ($diff->changedIndexes AS $index) { + $sql[] = $this->getDropIndexSQL($index, $tableName); + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + + return $sql; + } + + /** + * Get declaration of a number of fields in bulk + * + * @param array $fields a multidimensional associative array. + * The first dimension determines the field name, while the second + * dimension is keyed with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * + * @return string + */ + public function getColumnDeclarationListSQL(array $fields) + { + $queryFields = array(); + foreach ($fields as $fieldName => $field) { + $query = $this->getColumnDeclarationSQL($fieldName, $field); + $queryFields[] = $query; + } + return implode(', ', $queryFields); + } + + /** + * Obtain DBMS specific SQL code portion needed to declare a generic type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param array $field associative array with the name of the properties + * of the field being declared as array indexes. Currently, the types + * of supported field properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * field. If this argument is missing the field should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this field. + * + * notnull + * Boolean flag that indicates whether this field is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this field. + * collation + * Text value with the default COLLATION for this field. + * unique + * unique constraint + * check + * column check constraint + * columnDefinition + * a string that defines the complete column + * + * @return string DBMS specific SQL code portion that should be used to declare the column. + */ + public function getColumnDeclarationSQL($name, array $field) + { + if (isset($field['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSQL($field); + } else { + $default = $this->getDefaultValueDeclarationSQL($field); + + $charset = (isset($field['charset']) && $field['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSQL($field['charset']) : ''; + + $collation = (isset($field['collation']) && $field['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($field['collation']) : ''; + + $notnull = (isset($field['notnull']) && $field['notnull']) ? ' NOT NULL' : ''; + + $unique = (isset($field['unique']) && $field['unique']) ? + ' ' . $this->getUniqueFieldDeclarationSQL() : ''; + + $check = (isset($field['check']) && $field['check']) ? + ' ' . $field['check'] : ''; + + $typeDecl = $field['type']->getSqlDeclaration($field, $this); + $columnDef = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + } + + return $name . ' ' . $columnDef; + } + + /** + * Gets the SQL snippet that declares a floating point column of arbitrary precision. + * + * @param array $columnDef + * @return string + */ + public function getDecimalTypeDeclarationSQL(array $columnDef) + { + $columnDef['precision'] = ( ! isset($columnDef['precision']) || empty($columnDef['precision'])) + ? 10 : $columnDef['precision']; + $columnDef['scale'] = ( ! isset($columnDef['scale']) || empty($columnDef['scale'])) + ? 0 : $columnDef['scale']; + + return 'NUMERIC(' . $columnDef['precision'] . ', ' . $columnDef['scale'] . ')'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. + * + * @param array $field field definition array + * @return string DBMS specific SQL code portion needed to set a default value + */ + public function getDefaultValueDeclarationSQL($field) + { + $default = empty($field['notnull']) ? ' DEFAULT NULL' : ''; + + if (isset($field['default'])) { + $default = " DEFAULT '".$field['default']."'"; + if (isset($field['type'])) { + if (in_array((string)$field['type'], array("Integer", "BigInteger", "SmallInteger"))) { + $default = " DEFAULT ".$field['default']; + } else if ((string)$field['type'] == 'DateTime' && $field['default'] == $this->getCurrentTimestampSQL()) { + $default = " DEFAULT ".$this->getCurrentTimestampSQL(); + } + } + } + return $default; + } + + /** + * Obtain DBMS specific SQL code portion needed to set a CHECK constraint + * declaration to be used in statements like CREATE TABLE. + * + * @param array $definition check definition + * @return string DBMS specific SQL code portion needed to set a CHECK constraint + */ + public function getCheckDeclarationSQL(array $definition) + { + $constraints = array(); + foreach ($definition as $field => $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $field . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $field . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); + } + + /** + * Obtain DBMS specific SQL code portion needed to set a unique + * constraint declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the unique constraint + * @param Index $index index definition + * @return string DBMS specific SQL code portion needed + * to set a constraint + */ + public function getUniqueConstraintDeclarationSQL($name, Index $index) + { + if (count($index->getColumns()) == 0) { + throw \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return 'CONSTRAINT ' . $name . ' UNIQUE (' + . $this->getIndexFieldDeclarationListSQL($index->getColumns()) + . ')'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the index + * @param Index $index index definition + * @return string DBMS specific SQL code portion needed to set an index + */ + public function getIndexDeclarationSQL($name, Index $index) + { + $type = ''; + + if($index->isUnique()) { + $type = 'UNIQUE '; + } + + if (count($index->getColumns()) == 0) { + throw \InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return $type . 'INDEX ' . $name . ' (' + . $this->getIndexFieldDeclarationListSQL($index->getColumns()) + . ')'; + } + + /** + * getCustomTypeDeclarationSql + * Obtail SQL code portion needed to create a custom column, + * e.g. when a field has the "columnDefinition" keyword. + * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate. + * + * @return string + */ + public function getCustomTypeDeclarationSQL(array $columnDef) + { + return $columnDef['columnDefinition']; + } + + /** + * getIndexFieldDeclarationList + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @return string + */ + public function getIndexFieldDeclarationListSQL(array $fields) + { + $ret = array(); + foreach ($fields as $field => $definition) { + if (is_array($definition)) { + $ret[] = $field; + } else { + $ret[] = $definition; + } + } + return implode(', ', $ret); + } + + /** + * A method to return the required SQL string that fits between CREATE ... TABLE + * to create the table as a temporary table. + * + * Should be overridden in driver classes to return the correct string for the + * specific database type. + * + * The default is to return the string "TEMPORARY" - this will result in a + * SQL error for any database that does not support temporary tables, or that + * requires a different SQL command from "CREATE TEMPORARY TABLE". + * + * @return string The string required to be placed between "CREATE" and "TABLE" + * to generate a temporary table, if possible. + */ + public function getTemporaryTableSQL() + { + return 'TEMPORARY'; + } + + /** + * Some vendors require temporary table names to be qualified specially. + * + * @param string $tableName + * @return string + */ + public function getTemporaryTableName($tableName) + { + return $tableName; + } + + /** + * Get sql query to show a list of database. + * + * @return string + */ + public function getShowDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param array $definition an associative array with the following structure: + * name optional constraint name + * + * local the local field(s) + * + * foreign the foreign reference field(s) + * + * foreignTable the name of the foreign table + * + * onDelete referential delete action + * + * onUpdate referential update action + * + * deferred deferred constraint checking + * + * The onDelete and onUpdate keys accept the following values: + * + * CASCADE: Delete or update the row from the parent table and automatically delete or + * update the matching rows in the child table. Both ON DELETE CASCADE and ON UPDATE CASCADE are supported. + * Between two tables, you should not define several ON UPDATE CASCADE clauses that act on the same column + * in the parent table or in the child table. + * + * SET NULL: Delete or update the row from the parent table and set the foreign key column or columns in the + * child table to NULL. This is valid only if the foreign key columns do not have the NOT NULL qualifier + * specified. Both ON DELETE SET NULL and ON UPDATE SET NULL clauses are supported. + * + * NO ACTION: In standard SQL, NO ACTION means no action in the sense that an attempt to delete or update a primary + * key value is not allowed to proceed if there is a related foreign key value in the referenced table. + * + * RESTRICT: Rejects the delete or update operation for the parent table. NO ACTION and RESTRICT are the same as + * omitting the ON DELETE or ON UPDATE clause. + * + * SET DEFAULT + * + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration. + */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $sql; + } + + /** + * Return the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param ForeignKeyConstraint $foreignKey foreign key definition + * @return string + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($this->supportsForeignKeyOnUpdate() && $foreignKey->hasOption('onUpdate')) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); + } + if ($foreignKey->hasOption('onDelete')) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + return $query; + } + + /** + * returns given referential action in uppercase if valid, otherwise throws + * an exception + * + * @throws Doctrine_Exception_Exception if unknown referential action given + * @param string $action foreign key referential action + * @param string foreign key referential action in uppercase + */ + public function getForeignKeyReferentialActionSQL($action) + { + $upper = strtoupper($action); + switch ($upper) { + case 'CASCADE': + case 'SET NULL': + case 'NO ACTION': + case 'RESTRICT': + case 'SET DEFAULT': + return $upper; + break; + default: + throw \InvalidArgumentException('Invalid foreign key action: ' . $upper); + } + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param ForeignKeyConstraint $foreignKey + * @return string + */ + public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = ''; + if (strlen($foreignKey->getName())) { + $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; + } + $sql .= 'FOREIGN KEY ('; + + if (count($foreignKey->getLocalColumns()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'local' required."); + } + if (count($foreignKey->getForeignColumns()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreign' required."); + } + if (strlen($foreignKey->getForeignTableName()) == 0) { + throw new \InvalidArgumentException("Incomplete definition. 'foreignTable' required."); + } + + $sql .= implode(', ', $foreignKey->getLocalColumns()) + . ') REFERENCES ' + . $foreignKey->getForeignTableName() . '(' + . implode(', ', $foreignKey->getForeignColumns()) . ')'; + + return $sql; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration to be used in statements like CREATE TABLE. + * + * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a field declaration. + */ + public function getUniqueFieldDeclarationSQL() + { + return 'UNIQUE'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $charset name of the charset + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a field declaration. + */ + public function getColumnCharsetDeclarationSQL($charset) + { + return ''; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + public function getColumnCollationDeclarationSQL($collation) + { + return ''; + } + + /** + * Whether the platform prefers sequences for ID generation. + * Subclasses should override this method to return TRUE if they prefer sequences. + * + * @return boolean + */ + public function prefersSequences() + { + return false; + } + + /** + * Whether the platform prefers identity columns (eg. autoincrement) for ID generation. + * Subclasses should override this method to return TRUE if they prefer identity columns. + * + * @return boolean + */ + public function prefersIdentityColumns() + { + return false; + } + + /** + * Some platforms need the boolean values to be converted. + * + * The default conversion in this implementation converts to integers (false => 0, true => 1). + * + * @param mixed $item + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (is_bool($value)) { + $item[$k] = (int) $value; + } + } + } else if (is_bool($item)) { + $item = (int) $item; + } + return $item; + } + + /** + * Gets the SQL statement specific for the platform to set the charset. + * + * This function is MySQL specific and required by + * {@see \Doctrine\DBAL\Connection::setCharset($charset)} + * + * @param string $charset + * @return string + */ + public function getSetCharsetSQL($charset) + { + return "SET NAMES '".$charset."'"; + } + + /** + * Gets the SQL specific for the platform to get the current date. + * + * @return string + */ + public function getCurrentDateSQL() + { + return 'CURRENT_DATE'; + } + + /** + * Gets the SQL specific for the platform to get the current time. + * + * @return string + */ + public function getCurrentTimeSQL() + { + return 'CURRENT_TIME'; + } + + /** + * Gets the SQL specific for the platform to get the current timestamp + * + * @return string + */ + public function getCurrentTimestampSQL() + { + return 'CURRENT_TIMESTAMP'; + } + + /** + * Get sql for transaction isolation level Connection constant + * + * @param integer $level + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case Connection::TRANSACTION_READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + case Connection::TRANSACTION_READ_COMMITTED: + return 'READ COMMITTED'; + case Connection::TRANSACTION_REPEATABLE_READ: + return 'REPEATABLE READ'; + case Connection::TRANSACTION_SERIALIZABLE: + return 'SERIALIZABLE'; + default: + throw new \InvalidArgumentException('Invalid isolation level:' . $level); + } + } + + public function getListDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListSequencesSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableConstraintsSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableColumnsSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTablesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListUsersSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get the SQL to list all views of a database or user. + * + * @param string $database + * @return string + */ + public function getListViewsSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableIndexesSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableForeignKeysSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateViewSQL($name, $sql) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getDropViewSQL($name) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getDropSequenceSQL($sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getSequenceNextValSQL($sequenceName) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateDatabaseSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get sql to set the transaction isolation level + * + * @param integer $level + */ + public function getSetTransactionIsolationSQL($level) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create datetime fields in + * statements like CREATE TABLE + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create datetime with timezone offset fields. + * + * @param array $fieldDeclaration + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return $this->getDateTimeTypeDeclarationSQL($fieldDeclaration); + } + + + /** + * Obtain DBMS specific SQL to be used to create date fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Obtain DBMS specific SQL to be used to create time fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getFloatDeclarationSQL(array $fieldDeclaration) + { + return 'DOUBLE PRECISION'; + } + + /** + * Gets the default transaction isolation level of the platform. + * + * @return integer The default isolation level. + * @see Doctrine\DBAL\Connection\TRANSACTION_* constants. + */ + public function getDefaultTransactionIsolationLevel() + { + return Connection::TRANSACTION_READ_COMMITTED; + } + + /* supports*() metods */ + + /** + * Whether the platform supports sequences. + * + * @return boolean + */ + public function supportsSequences() + { + return false; + } + + /** + * Whether the platform supports identity columns. + * Identity columns are columns that recieve an auto-generated value from the + * database on insert of a row. + * + * @return boolean + */ + public function supportsIdentityColumns() + { + return false; + } + + /** + * Whether the platform supports indexes. + * + * @return boolean + */ + public function supportsIndexes() + { + return true; + } + + public function supportsAlterTable() + { + return true; + } + + /** + * Whether the platform supports transactions. + * + * @return boolean + */ + public function supportsTransactions() + { + return true; + } + + /** + * Whether the platform supports savepoints. + * + * @return boolean + */ + public function supportsSavepoints() + { + return true; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return boolean + */ + public function supportsReleaseSavepoints() + { + return $this->supportsSavepoints(); + } + + /** + * Whether the platform supports primary key constraints. + * + * @return boolean + */ + public function supportsPrimaryConstraints() + { + return true; + } + + /** + * Does the platform supports foreign key constraints? + * + * @return boolean + */ + public function supportsForeignKeyConstraints() + { + return true; + } + + /** + * Does this platform supports onUpdate in foreign key constraints? + * + * @return bool + */ + public function supportsForeignKeyOnUpdate() + { + return ($this->supportsForeignKeyConstraints() && true); + } + + /** + * Whether the platform supports database schemas. + * + * @return boolean + */ + public function supportsSchemas() + { + return false; + } + + /** + * Some databases don't allow to create and drop databases at all or only with certain tools. + * + * @return bool + */ + public function supportsCreateDropDatabase() + { + return true; + } + + /** + * Whether the platform supports getting the affected rows of a recent + * update/delete type query. + * + * @return boolean + */ + public function supportsGettingAffectedRows() + { + return true; + } + + public function getIdentityColumnNullInsertSQL() + { + return ""; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime value of this platform. + * + * @return string The format string. + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime with timezone value of this platform. + * + * @return string The format string. + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored date value of this platform. + * + * @return string The format string. + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored time value of this platform. + * + * @return string The format string. + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + public function modifyLimitQuery($query, $limit, $offset = null) + { + if ( ! is_null($limit)) { + $query .= ' LIMIT ' . $limit; + } + + if ( ! is_null($offset)) { + $query .= ' OFFSET ' . $offset; + } + + return $query; + } + + /** + * Gets the character casing of a column in an SQL result set of this platform. + * + * @param string $column The column name for which to get the correct character casing. + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSQLResultCasing($column) + { + return $column; + } + + /** + * Makes any fixes to a name of a schema element (table, sequence, ...) that are required + * by restrictions of the platform, like a maximum length. + * + * @param string $schemaName + * @return string + */ + public function fixSchemaElementName($schemaElementName) + { + return $schemaElementName; + } + + /** + * Maximum length of any given databse identifier, like tables or column names. + * + * @return int + */ + public function getMaxIdentifierLength() + { + return 63; + } + + /** + * Get the insert sql for an empty insert statement + * + * @param string $tableName + * @param string $identifierColumnName + * @return string $sql + */ + public function getEmptyIdentityInsertSQL($tableName, $identifierColumnName) + { + return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (null)'; + } + + /** + * Generate a Truncate Table SQL statement for a given table. + * + * Cascade is not supported on many platforms but would optionally cascade the truncate by + * following the foreign keys. + * + * @param string $tableName + * @param bool $cascade + * @return string + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE '.$tableName; + } + + /** + * This is for test reasons, many vendors have special requirements for dummy statements. + * + * @return string + */ + public function getDummySelectSQL() + { + return 'SELECT 1'; + } + + /** + * Generate SQL to create a new savepoint + * + * @param string $savepoint + * @return string + */ + public function createSavePoint($savepoint) + { + return 'SAVEPOINT ' . $savepoint; + } + + /** + * Generate SQL to release a savepoint + * + * @param string $savepoint + * @return string + */ + public function releaseSavePoint($savepoint) + { + return 'RELEASE SAVEPOINT ' . $savepoint; + } + + /** + * Generate SQL to rollback a savepoint + * + * @param string $savepoint + * @return string + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TO SAVEPOINT ' . $savepoint; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Platforms/DB2Platform.php b/Doctrine/DBAL/Platforms/DB2Platform.php new file mode 100644 index 0000000..7c4876f --- /dev/null +++ b/Doctrine/DBAL/Platforms/DB2Platform.php @@ -0,0 +1,564 @@ +. +*/ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\TableDiff; + +class DB2Platform extends AbstractPlatform +{ + public function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'smallint' => 'smallint', + 'bigint' => 'bigint', + 'integer' => 'integer', + 'time' => 'time', + 'date' => 'date', + 'varchar' => 'string', + 'character' => 'string', + 'clob' => 'text', + 'decimal' => 'decimal', + 'double' => 'float', + 'real' => 'float', + 'timestamp' => 'datetime', + ); + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column type. + * + * @param array $field + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( ! isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * + * @param array $field + */ + public function getClobTypeDeclarationSQL(array $field) + { + // todo clob(n) with $field['length']; + return 'CLOB(1M)'; + } + + /** + * Gets the name of the platform. + * + * @return string + */ + public function getName() + { + return 'db2'; + } + + + /** + * Gets the SQL snippet that declares a boolean column. + * + * @param array $columnDef + * @return string + */ + public function getBooleanTypeDeclarationSQL(array $columnDef) + { + return 'SMALLINT'; + } + + /** + * Gets the SQL snippet that declares a 4 byte integer column. + * + * @param array $columnDef + * @return string + */ + public function getIntegerTypeDeclarationSQL(array $columnDef) + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * Gets the SQL snippet that declares an 8 byte integer column. + * + * @param array $columnDef + * @return string + */ + public function getBigIntTypeDeclarationSQL(array $columnDef) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * Gets the SQL snippet that declares a 2 byte integer column. + * + * @param array $columnDef + * @return string + */ + public function getSmallIntTypeDeclarationSQL(array $columnDef) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($columnDef); + } + + /** + * Gets the SQL snippet that declares common properties of an integer column. + * + * @param array $columnDef + * @return string + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if ( ! empty($columnDef['autoincrement'])) { + $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; + } + return $autoinc; + } + + /** + * Obtain DBMS specific SQL to be used to create datetime fields in + * statements like CREATE TABLE + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) { + return "TIMESTAMP(0) WITH DEFAULT"; + } + + return 'TIMESTAMP(0)'; + } + + /** + * Obtain DBMS specific SQL to be used to create date fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * Obtain DBMS specific SQL to be used to create time fields in statements + * like CREATE TABLE. + * + * @param array $fieldDeclaration + * @return string + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + public function getListDatabasesSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListSequencesSQL($database) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getListTableConstraintsSQL($table) + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * This code fragment is originally from the Zend_Db_Adapter_Db2 class. + * + * @license New BSD License + * @param string $table + * @return string + */ + public function getListTableColumnsSQL($table) + { + return "SELECT DISTINCT c.tabschema, c.tabname, c.colname, c.colno, + c.typename, c.default, c.nulls, c.length, c.scale, + c.identity, tc.type AS tabconsttype, k.colseq + FROM syscat.columns c + LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc + ON (k.tabschema = tc.tabschema + AND k.tabname = tc.tabname + AND tc.type = 'P')) + ON (c.tabschema = k.tabschema + AND c.tabname = k.tabname + AND c.colname = k.colname) + WHERE UPPER(c.tabname) = UPPER('" . $table . "') ORDER BY c.colno"; + } + + public function getListTablesSQL() + { + return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = 'T'"; + } + + public function getListUsersSQL() + { + throw DBALException::notSupported(__METHOD__); + } + + /** + * Get the SQL to list all views of a database or user. + * + * @param string $database + * @return string + */ + public function getListViewsSQL($database) + { + return "SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS"; + } + + public function getListTableIndexesSQL($table) + { + return "SELECT NAME, COLNAMES, UNIQUERULE FROM SYSIBM.SYSINDEXES WHERE TBNAME = UPPER('" . $table . "')"; + } + + public function getListTableForeignKeysSQL($table) + { + return "SELECT TBNAME, RELNAME, REFTBNAME, DELETERULE, UPDATERULE, FKCOLNAMES, PKCOLNAMES ". + "FROM SYSIBM.SYSRELS WHERE TBNAME = UPPER('".$table."')"; + } + + public function getCreateViewSQL($name, $sql) + { + return "CREATE VIEW ".$name." AS ".$sql; + } + + public function getDropViewSQL($name) + { + return "DROP VIEW ".$name; + } + + public function getDropSequenceSQL($sequence) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getSequenceNextValSQL($sequenceName) + { + throw DBALException::notSupported(__METHOD__); + } + + public function getCreateDatabaseSQL($database) + { + return "CREATE DATABASE ".$database; + } + + public function getDropDatabaseSQL($database) + { + return "DROP DATABASE ".$database.";"; + } + + public function supportsCreateDropDatabase() + { + return false; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return boolean + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * Gets the SQL specific for the platform to get the current date. + * + * @return string + */ + public function getCurrentDateSQL() + { + return 'VALUES CURRENT DATE'; + } + + /** + * Gets the SQL specific for the platform to get the current time. + * + * @return string + */ + public function getCurrentTimeSQL() + { + return 'VALUES CURRENT TIME'; + } + + /** + * Gets the SQL specific for the platform to get the current timestamp + * + * @return string + */ + + public function getCurrentTimestampSQL() + { + return "VALUES CURRENT TIMESTAMP"; + } + + /** + * Obtain DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param string $name name of the index + * @param Index $index index definition + * @return string DBMS specific SQL code portion needed to set an index + */ + public function getIndexDeclarationSQL($name, Index $index) + { + return $this->getUniqueConstraintDeclarationSQL($name, $index); + } + + /** + * @param string $tableName + * @param array $columns + * @param array $options + * @return array + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $indexes = array(); + if (isset($options['indexes'])) { + $indexes = $options['indexes']; + } + $options['indexes'] = array(); + + $sqls = parent::_getCreateTableSQL($tableName, $columns, $options); + + foreach ($indexes as $index => $definition) { + $sqls[] = $this->getCreateIndexSQL($definition, $tableName); + } + return $sqls; + } + + /** + * Gets the SQL to alter an existing table. + * + * @param TableDiff $diff + * @return array + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + + $queryParts = array(); + foreach ($diff->addedColumns AS $fieldName => $column) { + $queryParts[] = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->removedColumns AS $column) { + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns AS $columnDiff) { + /* @var $columnDiff Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $queryParts[] = 'ALTER ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->renamedColumns AS $oldColumnName => $column) { + $queryParts[] = 'RENAME ' . $oldColumnName . ' TO ' . $column->getQuotedName($this); + } + + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . implode(" ", $queryParts); + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + if ($diff->newName !== false) { + $sql[] = 'RENAME TABLE TO ' . $diff->newName; + } + + return $sql; + } + + public function getDefaultValueDeclarationSQL($field) + { + if (isset($field['notnull']) && $field['notnull'] && !isset($field['default'])) { + if (in_array((string)$field['type'], array("Integer", "BigInteger", "SmallInteger"))) { + $field['default'] = 0; + } else if((string)$field['type'] == "DateTime") { + $field['default'] = "00-00-00 00:00:00"; + } else if ((string)$field['type'] == "Date") { + $field['default'] = "00-00-00"; + } else if((string)$field['type'] == "Time") { + $field['default'] = "00:00:00"; + } else { + $field['default'] = ''; + } + } + + unset($field['default']); // @todo this needs fixing + if (isset($field['version']) && $field['version']) { + if ((string)$field['type'] != "DateTime") { + $field['default'] = "1"; + } + } + + return parent::getDefaultValueDeclarationSQL($field); + } + + /** + * Get the insert sql for an empty insert statement + * + * @param string $tableName + * @param string $identifierColumnName + * @return string $sql + */ + public function getEmptyIdentityInsertSQL($tableName, $identifierColumnName) + { + return 'INSERT INTO ' . $tableName . ' (' . $identifierColumnName . ') VALUES (DEFAULT)'; + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "DECLARE GLOBAL TEMPORARY TABLE"; + } + + /** + * DB2 automatically moves temporary tables into the SESSION. schema. + * + * @param string $tableName + * @return string + */ + public function getTemporaryTableName($tableName) + { + return "SESSION." . $tableName; + } + + public function modifyLimitQuery($query, $limit, $offset = null) + { + if ($limit === null && $offset === null) { + return $query; + } + + $limit = (int)$limit; + $offset = (int)(($offset)?:0); + + // Todo OVER() needs ORDER BY data! + $sql = 'SELECT db22.* FROM (SELECT ROW_NUMBER() OVER() AS DC_ROWNUM, db21.* '. + 'FROM (' . $query . ') db21) db22 WHERE db22.DC_ROWNUM BETWEEN ' . ($offset+1) .' AND ' . ($offset+$limit); + return $sql; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } else { + return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')'; + } + } + + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function + * + * @param string $value an sql string literal or column name/alias + * @param integer $from where to start the substring portion + * @param integer $len the substring portion length + * @return string + */ + public function getSubstringExpression($value, $from, $len = null) + { + if ($len === null) + return 'SUBSTR(' . $value . ', ' . $from . ')'; + else { + return 'SUBSTR(' . $value . ', ' . $from . ', ' . $len . ')'; + } + } + + public function supportsIdentityColumns() + { + return true; + } + + public function prefersIdentityColumns() + { + return true; + } + + /** + * Gets the character casing of a column in an SQL result set of this platform. + * + * DB2 returns all column names in SQL result sets in uppercase. + * + * @param string $column The column name for which to get the correct character casing. + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSQLResultCasing($column) + { + return strtoupper($column); + } + + public function getForUpdateSQL() + { + return ' WITH RR USE AND KEEP UPDATE LOCKS'; + } + + public function getDummySelectSQL() + { + return 'SELECT 1 FROM sysibm.sysdummy1'; + } + + /** + * DB2 supports savepoints, but they work semantically different than on other vendor platforms. + * + * TODO: We have to investigate how to get DB2 up and running with savepoints. + * + * @return bool + */ + public function supportsSavepoints() + { + return false; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Platforms/MsSqlPlatform.php b/Doctrine/DBAL/Platforms/MsSqlPlatform.php new file mode 100644 index 0000000..ee92f40 --- /dev/null +++ b/Doctrine/DBAL/Platforms/MsSqlPlatform.php @@ -0,0 +1,788 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\TableDiff; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Schema\Index, Doctrine\DBAL\Schema\Table; + +/** + * The MsSqlPlatform provides the behavior, features and SQL dialect of the + * MySQL database platform. + * + * @since 2.0 + * @author Roman Borschel + * @author Jonathan H. Wage + * @author Benjamin Eberlei + * @todo Rename: MsSQLPlatform + */ +class MsSqlPlatform extends AbstractPlatform +{ + + /** + * Whether the platform prefers identity columns for ID generation. + * MsSql prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + * + * @return boolean + * @override + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * Whether the platform supports identity columns. + * MsSql supports this through AUTO_INCREMENT columns. + * + * @return boolean + * @override + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return boolean + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * create a new database + * + * @param string $name name of the database that should be created + * @return string + * @override + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * drop an existing database + * + * @param string $name name of the database that should be dropped + * @return string + * @override + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + /** + * @override + */ + public function supportsCreateDropDatabase() + { + return false; + } + + /** + * @override + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * @override + */ + public function getDropIndexSQL($index, $table=null) + { + if ($index instanceof \Doctrine\DBAL\Schema\Index) { + $index_ = $index; + $index = $index->getQuotedName($this); + } else if (!is_string($index)) { + throw new \InvalidArgumentException('AbstractPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + if (!isset($table)) { + return 'DROP INDEX ' . $index; + } else { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return "IF EXISTS (SELECT * FROM sysobjects WHERE name = '$index') + ALTER TABLE " . $table . " DROP CONSTRAINT " . $index . " + ELSE + DROP INDEX " . $index . " ON " . $table; + } + } + + /** + * @override + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + // @todo does other code breaks because of this? + // foce primary keys to be not null + foreach ($columns as &$column) { + if (isset($column['primary']) && $column['primary']) { + $column['notnull'] = true; + } + } + + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && !empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $name => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($name, $definition); + } + } + + if (isset($options['primary']) && !empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if (!empty($check)) { + $query .= ', ' . $check; + } + $query .= ')'; + + $sql[] = $query; + + if (isset($options['indexes']) && !empty($options['indexes'])) { + foreach ($options['indexes'] AS $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + } + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] AS $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * @override + */ + public function getUniqueConstraintDeclarationSQL($name, Index $index) + { + $constraint = parent::getUniqueConstraintDeclarationSQL($name, $index); + + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + + return $constraint; + } + + /** + * @override + */ + public function getCreateIndexSQL(Index $index, $table) + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + /** + * Extend unique key constraint with required filters + * + * @param string $sql + * @param Index $index + * @return string + */ + private function _appendUniqueConstraintDefinition($sql, Index $index) + { + $fields = array(); + foreach ($index->getColumns() as $field => $definition) { + if (!is_array($definition)) { + $field = $definition; + } + + $fields[] = $field . ' IS NOT NULL'; + } + + return $sql . ' WHERE ' . implode(' AND ', $fields); + } + + /** + * @override + */ + public function getAlterTableSQL(TableDiff $diff) + { + $queryParts = array(); + if ($diff->newName !== false) { + $queryParts[] = 'RENAME TO ' . $diff->newName; + } + + foreach ($diff->addedColumns AS $fieldName => $column) { + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->removedColumns AS $column) { + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns AS $columnDiff) { + /* @var $columnDiff Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $queryParts[] = 'CHANGE ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->renamedColumns AS $oldColumnName => $column) { + $queryParts[] = 'CHANGE ' . $oldColumnName . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + $sql = array(); + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + return $sql; + } + + /** + * @override + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** + * @override + */ + public function getShowDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + /** + * @override + */ + public function getListTablesSQL() + { + return "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name"; + } + + /** + * @override + */ + public function getListTableColumnsSQL($table) + { + return 'exec sp_columns @table_name = ' . $table; + } + + /** + * @override + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return "SELECT f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id + WHERE OBJECT_NAME (f.parent_object_id) = '" . $table . "'"; + } + + /** + * @override + */ + public function getListTableIndexesSQL($table) + { + return "exec sp_helpindex '" . $table . "'"; + } + + /** + * @override + */ + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + /** + * @override + */ + public function getListViewsSQL($database) + { + return "SELECT name FROM sysobjects WHERE type = 'V' ORDER BY name"; + } + + /** + * @override + */ + public function getDropViewSQL($name) + { + return 'DROP VIEW ' . $name; + } + + /** + * Returns the regular expression operator. + * + * @return string + * @override + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + * @override + */ + public function getGuidExpression() + { + return 'UUID()'; + } + + /** + * @override + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'CHARINDEX(' . $substr . ', ' . $str . ')'; + } else { + return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + } + + /** + * @override + */ + public function getModExpression($expression1, $expression2) + { + return $expression1 . ' % ' . $expression2; + } + + /** + * @override + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + $trimFn = ''; + + if (!$char) { + if ($pos == self::TRIM_LEADING) { + $trimFn = 'LTRIM'; + } else if ($pos == self::TRIM_TRAILING) { + $trimFn = 'RTRIM'; + } else { + return 'LTRIM(RTRIM(' . $str . '))'; + } + + return $trimFn . '(' . $str . ')'; + } else { + /** Original query used to get those expressions + declare @c varchar(100) = 'xxxBarxxx', @trim_char char(1) = 'x'; + declare @pat varchar(10) = '%[^' + @trim_char + ']%'; + select @c as string + , @trim_char as trim_char + , stuff(@c, 1, patindex(@pat, @c) - 1, null) as trim_leading + , reverse(stuff(reverse(@c), 1, patindex(@pat, reverse(@c)) - 1, null)) as trim_trailing + , reverse(stuff(reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null)), 1, patindex(@pat, reverse(stuff(@c, 1, patindex(@pat, @c) - 1, null))) - 1, null)) as trim_both; + */ + $pattern = "'%[^' + $char + ']%'"; + + if ($pos == self::TRIM_LEADING) { + return 'stuff(' . $str . ', 1, patindex(' . $pattern .', ' . $str . ') - 1, null)'; + } else if ($pos == self::TRIM_TRAILING) { + return 'reverse(stuff(reverse(' . $str . '), 1, patindex(' . $pattern .', reverse(' . $str . ')) - 1, null))'; + } else { + return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern .', ' . $str . ') - 1, null)), 1, patindex(' . $pattern .', reverse(stuff(' . $str . ', 1, patindex(' . $pattern .', ' . $str . ') - 1, null))) - 1, null))'; + } + } + } + + /** + * @override + */ + public function getConcatExpression() + { + $args = func_get_args(); + return '(' . implode(' + ', $args) . ')'; + } + + public function getListDatabasesSQL() + { + return 'SELECT * FROM SYS.DATABASES'; + } + + /** + * @override + */ + public function getSubstringExpression($value, $from, $len = null) + { + if (!is_null($len)) { + return 'SUBSTRING(' . $value . ', ' . $from . ', ' . $len . ')'; + } + return 'SUBSTRING(' . $value . ', ' . $from . ', LEN(' . $value . ') - ' . $from . ' + 1)'; + } + + /** + * @override + */ + public function getLengthExpression($column) + { + return 'LEN(' . $column . ')'; + } + + /** + * @override + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * @override + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** @override */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if (!isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'NCHAR(' . $length . ')' : 'CHAR(255)') : ($length ? 'NVARCHAR(' . $length . ')' : 'NTEXT'); + } + + /** @override */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'TEXT'; + } + + /** + * @override + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if (!empty($columnDef['autoincrement'])) { + $autoinc = ' IDENTITY'; + } + $unsigned = (isset($columnDef['unsigned']) && $columnDef['unsigned']) ? ' UNSIGNED' : ''; + + return $unsigned . $autoinc; + } + + /** + * @override + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + // 6 - microseconds precision length + return 'DATETIME2(6)'; + } + + /** + * @override + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME(0)'; + } + + /** + * @override + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BIT'; + } + + /** + * Adds an adapter-specific LIMIT clause to the SELECT statement. + * + * @param string $query + * @param mixed $limit + * @param mixed $offset + * @link http://lists.bestpractical.com/pipermail/rt-devel/2005-June/007339.html + * @return string + */ + public function modifyLimitQuery($query, $limit, $offset = null) + { + if ($limit > 0) { + $count = intval($limit); + $offset = intval($offset); + + if ($offset < 0) { + throw new Doctrine_Connection_Exception("LIMIT argument offset=$offset is not valid"); + } + + if ($offset == 0) { + $query = preg_replace('/^SELECT\s/i', 'SELECT TOP ' . $count . ' ', $query); + } else { + $orderby = stristr($query, 'ORDER BY'); + + if (!$orderby) { + $over = 'ORDER BY (SELECT 0)'; + } else { + $over = preg_replace('/\"[^,]*\".\"([^,]*)\"/i', '"inner_tbl"."$1"', $orderby); + } + + // Remove ORDER BY clause from $query + $query = preg_replace('/\s+ORDER BY(.*)/', '', $query); + + // Add ORDER BY clause as an argument for ROW_NUMBER() + $query = "SELECT ROW_NUMBER() OVER ($over) AS \"doctrine_rownum\", * FROM ($query) AS inner_tbl"; + + $start = $offset + 1; + $end = $offset + $count; + + $query = "WITH outer_tbl AS ($query) SELECT * FROM outer_tbl WHERE \"doctrine_rownum\" BETWEEN $start AND $end"; + } + } + + return $query; + } + + /** + * @override + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (is_bool($value) || is_numeric($item)) { + $item[$key] = ($value) ? 'TRUE' : 'FALSE'; + } + } + } else { + if (is_bool($item) || is_numeric($item)) { + $item = ($item) ? 'TRUE' : 'FALSE'; + } + } + return $item; + } + + /** + * @override + */ + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE TABLE"; + } + + /** + * @override + */ + public function getTemporaryTableName($tableName) + { + return '#' . $tableName; + } + + /** + * @override + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s.u'; + } + + /** + * @override + */ + public function getDateTimeTzFormatString() + { + return $this->getDateTimeFormatString(); + } + + /** + * Get the platform name for this instance + * + * @return string + */ + public function getName() + { + return 'mssql'; + } + + /** + * @override + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'bigint' => 'bigint', + 'numeric' => 'decimal', + 'bit' => 'boolean', + 'smallint' => 'smallint', + 'decimal' => 'decimal', + 'smallmoney' => 'integer', + 'int' => 'integer', + 'tinyint' => 'smallint', + 'money' => 'integer', + 'float' => 'float', + 'real' => 'float', + 'double' => 'float', + 'double precision' => 'float', + 'date' => 'date', + 'datetimeoffset' => 'datetimetz', + 'datetime2' => 'datetime', + 'smalldatetime' => 'datetime', + 'datetime' => 'datetime', + 'time' => 'time', + 'char' => 'string', + 'varchar' => 'string', + 'text' => 'text', + 'nchar' => 'string', + 'nvarchar' => 'string', + 'ntext' => 'text', + 'binary' => 'text', + 'varbinary' => 'text', + 'image' => 'text', + ); + } + + /** + * Generate SQL to create a new savepoint + * + * @param string $savepoint + * @return string + */ + public function createSavePoint($savepoint) + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + /** + * Generate SQL to release a savepoint + * + * @param string $savepoint + * @return string + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * Generate SQL to rollback a savepoint + * + * @param string $savepoint + * @return string + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** + * @override + */ + public function appendLockHint($fromClause, $lockMode) + { + // @todo coorect + if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_READ) { + return $fromClause . ' WITH (tablockx)'; + } else if ($lockMode == \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE) { + return $fromClause . ' WITH (tablockx)'; + } + else { + return $fromClause; + } + } + + /** + * @override + */ + public function getForUpdateSQL() + { + return ' '; + } +} diff --git a/Doctrine/DBAL/Platforms/MySqlPlatform.php b/Doctrine/DBAL/Platforms/MySqlPlatform.php new file mode 100644 index 0000000..f5b6c10 --- /dev/null +++ b/Doctrine/DBAL/Platforms/MySqlPlatform.php @@ -0,0 +1,602 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException, + Doctrine\DBAL\Schema\TableDiff; + +/** + * The MySqlPlatform provides the behavior, features and SQL dialect of the + * MySQL database platform. This platform represents a MySQL 5.0 or greater platform that + * uses the InnoDB storage engine. + * + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei + * @todo Rename: MySQLPlatform + */ +class MySqlPlatform extends AbstractPlatform +{ + /** + * Gets the character used for identifier quoting. + * + * @return string + * @override + */ + public function getIdentifierQuoteCharacter() + { + return '`'; + } + + /** + * Returns the regular expression operator. + * + * @return string + * @override + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + * @override + */ + public function getGuidExpression() + { + return 'UUID()'; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } else { + return 'LOCATE(' . $substr . ', ' . $str . ', '.$startPos.')'; + } + } + + /** + * Returns a series of strings concatinated + * + * concat() accepts an arbitrary number of parameters. Each parameter + * must contain an expression or an array with expressions. + * + * @param string|array(string) strings that will be concatinated. + * @override + */ + public function getConcatExpression() + { + $args = func_get_args(); + return 'CONCAT(' . join(', ', (array) $args) . ')'; + } + + public function getListDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + public function getListTableConstraintsSQL($table) + { + return 'SHOW INDEX FROM ' . $table; + } + + public function getListTableIndexesSQL($table) + { + return 'SHOW INDEX FROM ' . $table; + } + + public function getListViewsSQL($database) + { + return "SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = '".$database."'"; + } + + public function getListTableForeignKeysSQL($table, $database = null) + { + $sql = "SELECT DISTINCT k.`CONSTRAINT_NAME`, k.`COLUMN_NAME`, k.`REFERENCED_TABLE_NAME`, ". + "k.`REFERENCED_COLUMN_NAME` /*!50116 , c.update_rule, c.delete_rule */ ". + "FROM information_schema.key_column_usage k /*!50116 ". + "INNER JOIN information_schema.referential_constraints c ON ". + " c.constraint_name = k.constraint_name AND ". + " c.table_name = '$table' */ WHERE k.table_name = '$table'"; + + if ($database) { + $sql .= " AND k.table_schema = '$database' /*!50116 AND c.constraint_schema = '$database' */"; + } + + $sql .= " AND k.`REFERENCED_COLUMN_NAME` is not NULL"; + + return $sql; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column on the MySql platform. + * + * @params array $field + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( ! isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** @override */ + public function getClobTypeDeclarationSQL(array $field) + { + if ( ! empty($field['length']) && is_numeric($field['length'])) { + $length = $field['length']; + if ($length <= 255) { + return 'TINYTEXT'; + } else if ($length <= 65532) { + return 'TEXT'; + } else if ($length <= 16777215) { + return 'MEDIUMTEXT'; + } + } + return 'LONGTEXT'; + } + + /** + * @override + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + if (isset($fieldDeclaration['version']) && $fieldDeclaration['version'] == true) { + return 'TIMESTAMP'; + } else { + return 'DATETIME'; + } + } + + /** + * @override + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + /** + * @override + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'TINYINT(1)'; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration to be used in statements like CREATE TABLE. + * + * @param string $collation name of the collation + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a field declaration. + */ + public function getCollationFieldDeclaration($collation) + { + return 'COLLATE ' . $collation; + } + + /** + * Whether the platform prefers identity columns for ID generation. + * MySql prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + * + * @return boolean + * @override + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * Whether the platform supports identity columns. + * MySql supports this through AUTO_INCREMENT columns. + * + * @return boolean + * @override + */ + public function supportsIdentityColumns() + { + return true; + } + + public function getShowDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + public function getListTablesSQL() + { + return 'SHOW FULL TABLES WHERE Table_type = "BASE TABLE"'; + } + + public function getListTableColumnsSQL($table) + { + return 'DESCRIBE ' . $table; + } + + /** + * create a new database + * + * @param string $name name of the database that should be created + * @return string + * @override + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * drop an existing database + * + * @param string $name name of the database that should be dropped + * @return string + * @override + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + /** + * create a new table + * + * @param string $tableName Name of the database that should be created + * @param array $columns Associative array that contains the definition of each field of the new table + * The indexes of the array entries are the names of the fields of the table an + * the array entry values are associative arrays like those that are meant to be + * passed with the field definitions to get[Type]Declaration() functions. + * array( + * 'id' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * 'notnull' => 1 + * 'default' => 0 + * ), + * 'name' => array( + * 'type' => 'text', + * 'length' => 12 + * ), + * 'password' => array( + * 'type' => 'text', + * 'length' => 12 + * ) + * ); + * @param array $options An associative array of table options: + * array( + * 'comment' => 'Foo', + * 'charset' => 'utf8', + * 'collate' => 'utf8_unicode_ci', + * 'type' => 'innodb', + * ); + * + * @return void + * @override + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $index => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition); + } + } + + // add all indexes + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach($options['indexes'] as $index => $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($index, $definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE '; + if (!empty($options['temporary'])) { + $query .= 'TEMPORARY '; + } + $query.= 'TABLE ' . $tableName . ' (' . $queryFields . ')'; + + $optionStrings = array(); + + if (isset($options['comment'])) { + $optionStrings['comment'] = 'COMMENT = ' . $this->quote($options['comment'], 'text'); + } + if (isset($options['charset'])) { + $optionStrings['charset'] = 'DEFAULT CHARACTER SET ' . $options['charset']; + if (isset($options['collate'])) { + $optionStrings['charset'] .= ' COLLATE ' . $options['collate']; + } + } + + // get the type of the table + if (isset($options['engine'])) { + $optionStrings[] = 'ENGINE = ' . $options['engine']; + } else { + // default to innodb + $optionStrings[] = 'ENGINE = InnoDB'; + } + + if ( ! empty($optionStrings)) { + $query.= ' '.implode(' ', $optionStrings); + } + $sql[] = $query; + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * Gets the SQL to alter an existing table. + * + * @param TableDiff $diff + * @return array + */ + public function getAlterTableSQL(TableDiff $diff) + { + $queryParts = array(); + if ($diff->newName !== false) { + $queryParts[] = 'RENAME TO ' . $diff->newName; + } + + foreach ($diff->addedColumns AS $fieldName => $column) { + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->removedColumns AS $column) { + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->changedColumns AS $columnDiff) { + /* @var $columnDiff Doctrine\DBAL\Schema\ColumnDiff */ + $column = $columnDiff->column; + $queryParts[] = 'CHANGE ' . ($columnDiff->oldColumnName) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + foreach ($diff->renamedColumns AS $oldColumnName => $column) { + $queryParts[] = 'CHANGE ' . $oldColumnName . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + + $sql = array(); + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . implode(", ", $queryParts); + } + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + return $sql; + } + + /** + * Obtain DBMS specific SQL code portion needed to declare an integer type + * field to be used in statements like CREATE TABLE. + * + * @param string $name name the field to be declared. + * @param string $field associative array with the name of the properties + * of the field being declared as array indexes. + * Currently, the types of supported field + * properties are as follows: + * + * unsigned + * Boolean flag that indicates whether the field + * should be declared as unsigned integer if + * possible. + * + * default + * Integer value to be used as default for this + * field. + * + * notnull + * Boolean flag that indicates whether this field is + * constrained to not be set to null. + * @return string DBMS specific SQL code portion that should be used to + * declare the specified field. + * @override + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** @override */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** @override */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** @override */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ''; + if ( ! empty($columnDef['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + $unsigned = (isset($columnDef['unsigned']) && $columnDef['unsigned']) ? ' UNSIGNED' : ''; + + return $unsigned . $autoinc; + } + + /** + * Return the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param ForeignKeyConstraint $foreignKey + * @return string + * @override + */ + public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + return $query; + } + + /** + * Gets the SQL to drop an index of a table. + * + * @param Index $index name of the index to be dropped + * @param string|Table $table name of table that should be used in method + * @override + */ + public function getDropIndexSQL($index, $table=null) + { + if($index instanceof \Doctrine\DBAL\Schema\Index) { + $index = $index->getQuotedName($this); + } else if(!is_string($index)) { + throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $index parameter to be string or \Doctrine\DBAL\Schema\Index.'); + } + + if($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('MysqlPlatform::getDropIndexSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + return 'DROP INDEX ' . $index . ' ON ' . $table; + } + + /** + * Gets the SQL to drop a table. + * + * @param string $table The name of table to drop. + * @override + */ + public function getDropTableSQL($table) + { + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } else if(!is_string($table)) { + throw new \InvalidArgumentException('MysqlPlatform::getDropTableSQL() expects $table parameter to be string or \Doctrine\DBAL\Schema\Table.'); + } + + return 'DROP TABLE ' . $table; + } + + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * Get the platform name for this instance. + * + * @return string + */ + public function getName() + { + return 'mysql'; + } + + public function getReadLockSQL() + { + return 'LOCK IN SHARE MODE'; + } + + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'tinyint' => 'boolean', + 'smallint' => 'smallint', + 'mediumint' => 'integer', + 'int' => 'integer', + 'integer' => 'integer', + 'bigint' => 'bigint', + 'tinytext' => 'text', + 'mediumtext' => 'text', + 'longtext' => 'text', + 'text' => 'text', + 'varchar' => 'string', + 'string' => 'string', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'time' => 'time', + 'float' => 'float', + 'double' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'numeric' => 'decimal', + 'year' => 'date', + ); + } +} diff --git a/Doctrine/DBAL/Platforms/OraclePlatform.php b/Doctrine/DBAL/Platforms/OraclePlatform.php new file mode 100644 index 0000000..c3c4c00 --- /dev/null +++ b/Doctrine/DBAL/Platforms/OraclePlatform.php @@ -0,0 +1,724 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\TableDiff; + +/** + * OraclePlatform. + * + * @since 2.0 + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + */ +class OraclePlatform extends AbstractPlatform +{ + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * @param string $value an sql string literal or column name/alias + * @param integer $position where to start the substring portion + * @param integer $length the substring portion length + * @return string SQL substring function with given parameters + * @override + */ + public function getSubstringExpression($value, $position, $length = null) + { + if ($length !== null) { + return "SUBSTR($value, $position, $length)"; + } + + return "SUBSTR($value, $position)"; + } + + /** + * Return string to call a variable with the current timestamp inside an SQL statement + * There are three special variables for current date and time: + * - CURRENT_TIMESTAMP (date and time, TIMESTAMP type) + * - CURRENT_DATE (date, DATE type) + * - CURRENT_TIME (time, TIME type) + * + * @return string to call a variable with the current timestamp + * @override + */ + public function getNowExpression($type = 'timestamp') + { + switch ($type) { + case 'date': + case 'time': + case 'timestamp': + default: + return 'TO_CHAR(CURRENT_TIMESTAMP, \'YYYY-MM-DD HH24:MI:SS\')'; + } + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'INSTR('.$str.', '.$substr.')'; + } else { + return 'INSTR('.$str.', '.$substr.', '.$startPos.')'; + } + } + + /** + * Returns global unique identifier + * + * @return string to get global unique identifier + * @override + */ + public function getGuidExpression() + { + return 'SYS_GUID()'; + } + + /** + * Gets the SQL used to create a sequence that starts with a given value + * and increments by the given allocation size. + * + * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. + * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection + * in {@see listSequences()} + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @return string + */ + public function getCreateSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + /** + * {@inheritdoc} + * + * @param string $sequenceName + * @override + */ + public function getSequenceNextValSQL($sequenceName) + { + return 'SELECT ' . $sequenceName . '.nextval FROM DUAL'; + } + + /** + * {@inheritdoc} + * + * @param integer $level + * @override + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case \Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + case \Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED: + return 'READ COMMITTED'; + case \Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ: + case \Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE: + return 'SERIALIZABLE'; + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + /** + * @override + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'NUMBER(1)'; + } + + /** + * @override + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return 'NUMBER(10)'; + } + + /** + * @override + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return 'NUMBER(20)'; + } + + /** + * @override + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'NUMBER(5)'; + } + + /** + * @override + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0)'; + } + + /** + * @override + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * @override + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return ''; + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column on the Oracle platform. + * + * @params array $field + * @override + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( ! isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(2000)') + : ($length ? 'VARCHAR2(' . $length . ')' : 'VARCHAR2(4000)'); + } + + /** @override */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'CLOB'; + } + + public function getListDatabasesSQL() + { + return 'SELECT username FROM all_users'; + } + + public function getListSequencesSQL($database) + { + return "SELECT sequence_name, min_value, increment_by FROM sys.all_sequences ". + "WHERE SEQUENCE_OWNER = '".strtoupper($database)."'"; + } + + /** + * + * @param string $table + * @param array $columns + * @param array $options + * @return array + */ + protected function _getCreateTableSQL($table, array $columns, array $options = array()) + { + $indexes = isset($options['indexes']) ? $options['indexes'] : array(); + $options['indexes'] = array(); + $sql = parent::_getCreateTableSQL($table, $columns, $options); + + foreach ($columns as $name => $column) { + if (isset($column['sequence'])) { + $sql[] = $this->getCreateSequenceSQL($column['sequence'], 1); + } + + if (isset($column['autoincrement']) && $column['autoincrement'] || + (isset($column['autoinc']) && $column['autoinc'])) { + $sql = array_merge($sql, $this->getCreateAutoincrementSql($name, $table)); + } + } + + if (isset($indexes) && ! empty($indexes)) { + foreach ($indexes as $indexName => $index) { + $sql[] = $this->getCreateIndexSQL($index, $table); + } + } + + return $sql; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaOracleReader.html + * @param string $table + * @return string + */ + public function getListTableIndexesSQL($table) + { + $table = strtoupper($table); + + return "SELECT uind.index_name AS name, " . + " uind.index_type AS type, " . + " decode( uind.uniqueness, 'NONUNIQUE', 0, 'UNIQUE', 1 ) AS is_unique, " . + " uind_col.column_name AS column_name, " . + " uind_col.column_position AS column_pos, " . + " (SELECT ucon.constraint_type FROM user_constraints ucon WHERE ucon.constraint_name = uind.index_name) AS is_primary ". + "FROM user_indexes uind, user_ind_columns uind_col " . + "WHERE uind.index_name = uind_col.index_name AND uind_col.table_name = '$table' ORDER BY uind_col.column_position ASC"; + } + + public function getListTablesSQL() + { + return 'SELECT * FROM sys.user_tables'; + } + + public function getListViewsSQL($database) + { + return 'SELECT view_name, text FROM sys.user_views'; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + public function getCreateAutoincrementSql($name, $table, $start = 1) + { + $table = strtoupper($table); + $sql = array(); + + $indexName = $table . '_AI_PK'; + $definition = array( + 'primary' => true, + 'columns' => array($name => true), + ); + + $idx = new \Doctrine\DBAL\Schema\Index($indexName, array($name), true, true); + + $sql[] = 'DECLARE + constraints_Count NUMBER; +BEGIN + SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count FROM USER_CONSTRAINTS WHERE TABLE_NAME = \''.$table.'\' AND CONSTRAINT_TYPE = \'P\'; + IF constraints_Count = 0 OR constraints_Count = \'\' THEN + EXECUTE IMMEDIATE \''.$this->getCreateConstraintSQL($idx, $table).'\'; + END IF; +END;'; + + $sequenceName = $table . '_SEQ'; + $sequence = new \Doctrine\DBAL\Schema\Sequence($sequenceName, $start); + $sql[] = $this->getCreateSequenceSQL($sequence); + + $triggerName = $table . '_AI_PK'; + $sql[] = 'CREATE TRIGGER ' . $triggerName . ' + BEFORE INSERT + ON ' . $table . ' + FOR EACH ROW +DECLARE + last_Sequence NUMBER; + last_InsertID NUMBER; +BEGIN + SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $name . ' FROM DUAL; + IF (:NEW.' . $name . ' IS NULL OR :NEW.'.$name.' = 0) THEN + SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $name . ' FROM DUAL; + ELSE + SELECT NVL(Last_Number, 0) INTO last_Sequence + FROM User_Sequences + WHERE Sequence_Name = \'' . $sequenceName . '\'; + SELECT :NEW.' . $name . ' INTO last_InsertID FROM DUAL; + WHILE (last_InsertID > last_Sequence) LOOP + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END LOOP; + END IF; +END;'; + return $sql; + } + + public function getDropAutoincrementSql($table) + { + $table = strtoupper($table); + $trigger = $table . '_AI_PK'; + + if ($trigger) { + $sql[] = 'DROP TRIGGER ' . $trigger; + $sql[] = $this->getDropSequenceSQL($table.'_SEQ'); + + $indexName = $table . '_AI_PK'; + $sql[] = $this->getDropConstraintSQL($indexName, $table); + } + + return $sql; + } + + public function getListTableForeignKeysSQL($table) + { + $table = strtoupper($table); + + return "SELECT alc.constraint_name, + alc.DELETE_RULE, + alc.search_condition, + cols.column_name \"local_column\", + cols.position, + r_alc.table_name \"references_table\", + r_cols.column_name \"foreign_column\" + FROM all_cons_columns cols +LEFT JOIN all_constraints alc + ON alc.constraint_name = cols.constraint_name + AND alc.owner = cols.owner +LEFT JOIN all_constraints r_alc + ON alc.r_constraint_name = r_alc.constraint_name + AND alc.r_owner = r_alc.owner +LEFT JOIN all_cons_columns r_cols + ON r_alc.constraint_name = r_cols.constraint_name + AND r_alc.owner = r_cols.owner + AND cols.position = r_cols.position + WHERE alc.constraint_name = cols.constraint_name + AND alc.constraint_type = 'R' + AND alc.table_name = '".$table."'"; + } + + public function getListTableConstraintsSQL($table) + { + $table = strtoupper($table); + return 'SELECT * FROM user_constraints WHERE table_name = \'' . $table . '\''; + } + + public function getListTableColumnsSQL($table) + { + $table = strtoupper($table); + return "SELECT * FROM all_tab_columns WHERE table_name = '" . $table . "' ORDER BY column_name"; + } + + /** + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @return string + */ + public function getDropSequenceSQL($sequence) + { + if ($sequence instanceof \Doctrine\DBAL\Schema\Sequence) { + $sequence = $sequence->getQuotedName($this); + } + + return 'DROP SEQUENCE ' . $sequence; + } + + /** + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * @return string + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof \Doctrine\DBAL\Schema\ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + if ($table instanceof \Doctrine\DBAL\Schema\Table) { + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + public function getDropDatabaseSQL($database) + { + return 'DROP USER ' . $database . ' CASCADE'; + } + + /** + * Gets the sql statements for altering an existing table. + * + * The method returns an array of sql statements, since some platforms need several statements. + * + * @param string $diff->name name of the table that is intended to be changed. + * @param array $changes associative array that contains the details of each type * + * @param boolean $check indicates whether the function should just check if the DBMS driver + * can perform the requested table alterations if the value is true or + * actually perform them otherwise. + * @return array + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + + $fields = array(); + foreach ($diff->addedColumns AS $column) { + $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + } + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' ADD (' . implode(', ', $fields) . ')'; + } + + $fields = array(); + foreach ($diff->changedColumns AS $columnDiff) { + $column = $columnDiff->column; + $fields[] = $column->getQuotedName($this). ' ' . $this->getColumnDeclarationSQL('', $column->toArray()); + } + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' MODIFY (' . implode(', ', $fields) . ')'; + } + + foreach ($diff->renamedColumns AS $oldColumnName => $column) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME COLUMN ' . $oldColumnName .' TO ' . $column->getQuotedName($this); + } + + $fields = array(); + foreach ($diff->removedColumns AS $column) { + $fields[] = $column->getQuotedName($this); + } + if (count($fields)) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' DROP COLUMN ' . implode(', ', $fields); + } + + if ($diff->newName !== false) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME TO ' . $diff->newName; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + return $sql; + } + + /** + * Whether the platform prefers sequences for ID generation. + * + * @return boolean + */ + public function prefersSequences() + { + return true; + } + + /** + * Get the platform name for this instance + * + * @return string + */ + public function getName() + { + return 'oracle'; + } + + /** + * Adds an driver-specific LIMIT clause to the query + * + * @param string $query query to modify + * @param integer $limit limit the number of rows + * @param integer $offset start reading from given offset + * @return string the modified query + */ + public function modifyLimitQuery($query, $limit, $offset = null) + { + $limit = (int) $limit; + $offset = (int) $offset; + if (preg_match('/^\s*SELECT/i', $query)) { + if ( ! preg_match('/\sFROM\s/i', $query)) { + $query .= " FROM dual"; + } + if ($limit > 0) { + $max = $offset + $limit; + $column = '*'; + if ($offset > 0) { + $min = $offset + 1; + $query = 'SELECT b.'.$column.' FROM ('. + 'SELECT a.*, ROWNUM AS doctrine_rownum FROM (' + . $query . ') a '. + ') b '. + 'WHERE doctrine_rownum BETWEEN ' . $min . ' AND ' . $max; + } else { + $query = 'SELECT a.'.$column.' FROM (' . $query .') a WHERE ROWNUM <= ' . $max; + } + } + } + return $query; + } + + /** + * Gets the character casing of a column in an SQL result set of this platform. + * + * Oracle returns all column names in SQL result sets in uppercase. + * + * @param string $column The column name for which to get the correct character casing. + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSQLResultCasing($column) + { + return strtoupper($column); + } + + public function getCreateTemporaryTableSnippetSQL() + { + return "CREATE GLOBAL TEMPORARY TABLE"; + } + + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sP'; + } + + public function getDateFormatString() + { + return 'Y-m-d 00:00:00'; + } + + public function getTimeFormatString() + { + return '1900-01-01 H:i:s'; + } + + public function fixSchemaElementName($schemaElementName) + { + if (strlen($schemaElementName) > 30) { + // Trim it + return substr($schemaElementName, 0, 30); + } + return $schemaElementName; + } + + /** + * Maximum length of any given databse identifier, like tables or column names. + * + * @return int + */ + public function getMaxIdentifierLength() + { + return 30; + } + + /** + * Whether the platform supports sequences. + * + * @return boolean + */ + public function supportsSequences() + { + return true; + } + + public function supportsForeignKeyOnUpdate() + { + return false; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return boolean + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * @inheritdoc + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE TABLE '.$tableName; + } + + /** + * This is for test reasons, many vendors have special requirements for dummy statements. + * + * @return string + */ + public function getDummySelectSQL() + { + return 'SELECT 1 FROM DUAL'; + } + + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'integer' => 'integer', + 'number' => 'integer', + 'pls_integer' => 'boolean', + 'binary_integer' => 'boolean', + 'varchar' => 'string', + 'varchar2' => 'string', + 'nvarchar2' => 'string', + 'char' => 'string', + 'nchar' => 'string', + 'date' => 'datetime', + 'timestamp' => 'datetime', + 'timestamptz' => 'datetimetz', + 'float' => 'float', + 'long' => 'string', + 'clob' => 'text', + 'nclob' => 'text', + 'rowid' => 'string', + 'urowid' => 'string' + ); + } + + /** + * Generate SQL to release a savepoint + * + * @param string $savepoint + * @return string + */ + public function releaseSavePoint($savepoint) + { + return ''; + } +} diff --git a/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php b/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php new file mode 100644 index 0000000..6c57312 --- /dev/null +++ b/Doctrine/DBAL/Platforms/PostgreSqlPlatform.php @@ -0,0 +1,710 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\Schema\TableDiff, + Doctrine\DBAL\Schema\Table; + +/** + * PostgreSqlPlatform. + * + * @since 2.0 + * @author Roman Borschel + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @todo Rename: PostgreSQLPlatform + */ +class PostgreSqlPlatform extends AbstractPlatform +{ + /** + * Returns part of a string. + * + * Note: Not SQL92, but common functionality. + * + * @param string $value the target $value the string or the string column. + * @param int $from extract from this characeter. + * @param int $len extract this amount of characters. + * @return string sql that extracts part of a string. + * @override + */ + public function getSubstringExpression($value, $from, $len = null) + { + if ($len === null) { + return 'SUBSTR(' . $value . ', ' . $from . ')'; + } else { + return 'SUBSTR(' . $value . ', ' . $from . ', ' . $len . ')'; + } + } + + /** + * Returns the SQL string to return the current system date and time. + * + * @return string + */ + public function getNowExpression() + { + return 'LOCALTIMESTAMP(0)'; + } + + /** + * regexp + * + * @return string the regular expression operator + * @override + */ + public function getRegexpExpression() + { + return 'SIMILAR TO'; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos !== false) { + $str = $this->getSubstringExpression($str, $startPos); + return 'CASE WHEN (POSITION('.$substr.' IN '.$str.') = 0) THEN 0 ELSE (POSITION('.$substr.' IN '.$str.') + '.($startPos-1).') END'; + } else { + return 'POSITION('.$substr.' IN '.$str.')'; + } + } + + /** + * parses a literal boolean value and returns + * proper sql equivalent + * + * @param string $value boolean value to be parsed + * @return string parsed boolean value + */ + /*public function parseBoolean($value) + { + return $value; + }*/ + + /** + * Whether the platform supports sequences. + * Postgres has native support for sequences. + * + * @return boolean + */ + public function supportsSequences() + { + return true; + } + + /** + * Whether the platform supports database schemas. + * + * @return boolean + */ + public function supportsSchemas() + { + return true; + } + + /** + * Whether the platform supports identity columns. + * Postgres supports these through the SERIAL keyword. + * + * @return boolean + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * Whether the platform prefers sequences for ID generation. + * + * @return boolean + */ + public function prefersSequences() + { + return true; + } + + public function getListDatabasesSQL() + { + return 'SELECT datname FROM pg_database'; + } + + public function getListSequencesSQL($database) + { + return "SELECT + relname + FROM + pg_class + WHERE relkind = 'S' AND relnamespace IN + (SELECT oid FROM pg_namespace + WHERE nspname NOT LIKE 'pg_%' AND nspname != 'information_schema')"; + } + + public function getListTablesSQL() + { + return "SELECT + c.relname AS table_name + FROM pg_class c, pg_user u + WHERE c.relowner = u.usesysid + AND c.relkind = 'r' + AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) + AND c.relname !~ '^(pg_|sql_)' + UNION + SELECT c.relname AS table_name + FROM pg_class c + WHERE c.relkind = 'r' + AND NOT EXISTS (SELECT 1 FROM pg_views WHERE viewname = c.relname) + AND NOT EXISTS (SELECT 1 FROM pg_user WHERE usesysid = c.relowner) + AND c.relname !~ '^pg_'"; + } + + public function getListViewsSQL($database) + { + return 'SELECT viewname, definition FROM pg_views'; + } + + public function getListTableForeignKeysSQL($table, $database = null) + { + return "SELECT r.conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid = + ( + SELECT c.oid + FROM pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = '" . $table . "' AND pg_catalog.pg_table_is_visible(c.oid) + ) + AND r.contype = 'f'"; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + public function getListTableConstraintsSQL($table) + { + return "SELECT + relname + FROM + pg_class + WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname = '$table' + AND pg_class.oid = pg_index.indrelid + AND (indisunique = 't' OR indisprimary = 't') + )"; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param string $table + * @return string + */ + public function getListTableIndexesSQL($table) + { + return "SELECT relname, pg_index.indisunique, pg_index.indisprimary, + pg_index.indkey, pg_index.indrelid + FROM pg_class, pg_index + WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname='$table' AND pg_class.oid=pg_index.indrelid + ) AND pg_index.indexrelid = oid"; + } + + public function getListTableColumnsSQL($table) + { + return "SELECT + a.attnum, + a.attname AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM pg_catalog.pg_type t2 + WHERE t2.typtype = 'd' AND t2.typname = format_type(a.atttypid, a.atttypmod)) AS domain_complete_type, + a.attnotnull AS isnotnull, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_attrdef.adsrc + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default + FROM pg_attribute a, pg_class c, pg_type t + WHERE c.relname = '$table' + AND a.attnum > 0 + AND a.attrelid = c.oid + AND a.atttypid = t.oid + ORDER BY a.attnum"; + } + + /** + * create a new database + * + * @param string $name name of the database that should be created + * @throws PDOException + * @return void + * @override + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE DATABASE ' . $name; + } + + /** + * drop an existing database + * + * @param string $name name of the database that should be dropped + * @throws PDOException + * @access public + */ + public function getDropDatabaseSQL($name) + { + return 'DROP DATABASE ' . $name; + } + + /** + * Return the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey foreign key definition + * @return string + * @override + */ + public function getAdvancedForeignKeyOptionsSQL(\Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { + $query .= ' DEFERRABLE'; + } else { + $query .= ' NOT DEFERRABLE'; + } + if ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false) { + $query .= ' INITIALLY DEFERRED'; + } else { + $query .= ' INITIALLY IMMEDIATE'; + } + return $query; + } + + /** + * generates the sql for altering an existing table on postgresql + * + * @param string $name name of the table that is intended to be changed. + * @param array $changes associative array that contains the details of each type * + * @param boolean $check indicates whether the function should just check if the DBMS driver + * can perform the requested table alterations if the value is true or + * actually perform them otherwise. + * @see Doctrine_Export::alterTable() + * @return array + * @override + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = array(); + + foreach ($diff->addedColumns as $column) { + $query = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + foreach ($diff->removedColumns as $column) { + $query = 'DROP ' . $column->getQuotedName($this); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + + foreach ($diff->changedColumns AS $columnDiff) { + $oldColumnName = $columnDiff->oldColumnName; + $column = $columnDiff->column; + + if ($columnDiff->hasChanged('type')) { + $type = $column->getType(); + + // here was a server version check before, but DBAL API does not support this anymore. + $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSqlDeclaration($column->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + if ($columnDiff->hasChanged('default')) { + $query = 'ALTER ' . $oldColumnName . ' SET ' . $this->getDefaultValueDeclarationSQL($column->toArray()); + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + if ($columnDiff->hasChanged('notnull')) { + $query = 'ALTER ' . $oldColumnName . ' ' . ($column->getNotNull() ? 'SET' : 'DROP') . ' NOT NULL'; + $sql[] = 'ALTER TABLE ' . $diff->name . ' ' . $query; + } + if ($columnDiff->hasChanged('autoincrement')) { + if ($column->getAutoincrement()) { + // add autoincrement + $seqName = $diff->name . '_' . $oldColumnName . '_seq'; + + $sql[] = "CREATE SEQUENCE " . $seqName; + $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ") FROM " . $diff->name . "))"; + $query = "ALTER " . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')"; + $sql[] = "ALTER TABLE " . $diff->name . " " . $query; + } else { + // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have + $query = "ALTER " . $oldColumnName . " " . "DROP DEFAULT"; + $sql[] = "ALTER TABLE " . $diff->name . " " . $query; + } + } + } + + foreach ($diff->renamedColumns as $oldColumnName => $column) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME COLUMN ' . $oldColumnName . ' TO ' . $column->getQuotedName($this); + } + + if ($diff->newName !== false) { + $sql[] = 'ALTER TABLE ' . $diff->name . ' RENAME TO ' . $diff->newName; + } + + $sql = array_merge($sql, $this->_getAlterTableIndexForeignKeySQL($diff)); + + return $sql; + } + + /** + * Gets the SQL to create a sequence on this platform. + * + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @return string + */ + public function getCreateSequenceSQL(\Doctrine\DBAL\Schema\Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' START ' . $sequence->getInitialValue(); + } + + /** + * Drop existing sequence + * @param \Doctrine\DBAL\Schema\Sequence $sequence + * @return string + */ + public function getDropSequenceSQL($sequence) + { + if ($sequence instanceof \Doctrine\DBAL\Schema\Sequence) { + $sequence = $sequence->getQuotedName($this); + } + return 'DROP SEQUENCE ' . $sequence; + } + + /** + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * @return string + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + /** + * Gets the SQL used to create a table. + * + * @param unknown_type $tableName + * @param array $columns + * @param array $options + * @return unknown + */ + protected function _getCreateTableSQL($tableName, array $columns, array $options = array()) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE TABLE ' . $tableName . ' (' . $queryFields . ')'; + + $sql[] = $query; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] AS $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableName); + } + } + + if (isset($options['foreignKeys'])) { + foreach ((array) $options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $tableName); + } + } + + return $sql; + } + + /** + * Postgres wants boolean values converted to the strings 'true'/'false'. + * + * @param array $item + * @override + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (is_bool($value) || is_numeric($item)) { + $item[$key] = ($value) ? 'true' : 'false'; + } + } + } else { + if (is_bool($item) || is_numeric($item)) { + $item = ($item) ? 'true' : 'false'; + } + } + return $item; + } + + public function getSequenceNextValSQL($sequenceName) + { + return "SELECT NEXTVAL('" . $sequenceName . "')"; + } + + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' + . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * @override + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BOOLEAN'; + } + + /** + * @override + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + if ( ! empty($field['autoincrement'])) { + return 'SERIAL'; + } + + return 'INT'; + } + + /** + * @override + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + if ( ! empty($field['autoincrement'])) { + return 'BIGSERIAL'; + } + return 'BIGINT'; + } + + /** + * @override + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return 'SMALLINT'; + } + + /** + * @override + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITHOUT TIME ZONE'; + } + + /** + * @override + */ + public function getDateTimeTzTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * @override + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME(0) WITHOUT TIME ZONE'; + } + + /** + * @override + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + return ''; + } + + /** + * Gets the SQL snippet used to declare a VARCHAR column on the MySql platform. + * + * @params array $field + * @override + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( ! isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT'); + } + + /** @override */ + public function getClobTypeDeclarationSQL(array $field) + { + return 'TEXT'; + } + + /** + * Get the platform name for this instance + * + * @return string + */ + public function getName() + { + return 'postgresql'; + } + + /** + * Gets the character casing of a column in an SQL result set. + * + * PostgreSQL returns all column names in SQL result sets in lowercase. + * + * @param string $column The column name for which to get the correct character casing. + * @return string The column name in the character casing used in SQL result sets. + */ + public function getSQLResultCasing($column) + { + return strtolower($column); + } + + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sO'; + } + + /** + * Get the insert sql for an empty insert statement + * + * @param string $tableName + * @param string $identifierColumnName + * @return string $sql + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + /** + * @inheritdoc + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'TRUNCATE '.$tableName.' '.($cascade)?'CASCADE':''; + } + + public function getReadLockSQL() + { + return 'FOR SHARE'; + } + + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'smallint' => 'smallint', + 'int2' => 'smallint', + 'serial' => 'integer', + 'serial4' => 'integer', + 'int' => 'integer', + 'int4' => 'integer', + 'integer' => 'integer', + 'bigserial' => 'bigint', + 'serial8' => 'bigint', + 'bigint' => 'bigint', + 'int8' => 'bigint', + 'bool' => 'boolean', + 'boolean' => 'boolean', + 'text' => 'text', + 'varchar' => 'string', + 'interval' => 'string', + '_varchar' => 'string', + 'char' => 'string', + 'bpchar' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'timestamptz' => 'datetimetz', + 'time' => 'time', + 'timetz' => 'time', + 'float' => 'float', + 'float4' => 'float', + 'float8' => 'float', + 'double' => 'float', + 'double precision' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'money' => 'decimal', + 'numeric' => 'decimal', + 'year' => 'date', + ); + } +} diff --git a/Doctrine/DBAL/Platforms/SqlitePlatform.php b/Doctrine/DBAL/Platforms/SqlitePlatform.php new file mode 100644 index 0000000..b193cc0 --- /dev/null +++ b/Doctrine/DBAL/Platforms/SqlitePlatform.php @@ -0,0 +1,474 @@ +. + */ + +namespace Doctrine\DBAL\Platforms; + +use Doctrine\DBAL\DBALException; + +/** + * The SqlitePlatform class describes the specifics and dialects of the SQLite + * database platform. + * + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei + * @todo Rename: SQLitePlatform + */ +class SqlitePlatform extends AbstractPlatform +{ + /** + * returns the regular expression operator + * + * @return string + * @override + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * Return string to call a variable with the current timestamp inside an SQL statement + * There are three special variables for current date and time. + * + * @return string sqlite function as string + * @override + */ + public function getNowExpression($type = 'timestamp') + { + switch ($type) { + case 'time': + return 'time(\'now\')'; + case 'date': + return 'date(\'now\')'; + case 'timestamp': + default: + return 'datetime(\'now\')'; + } + } + + /** + * Trim a string, leading/trailing/both and with a given char which defaults to space. + * + * @param string $str + * @param int $pos + * @param string $char + * @return string + */ + public function getTrimExpression($str, $pos = self::TRIM_UNSPECIFIED, $char = false) + { + $trimFn = ''; + $trimChar = ($char != false) ? (', ' . $char) : ''; + + if ($pos == self::TRIM_LEADING) { + $trimFn = 'LTRIM'; + } else if($pos == self::TRIM_TRAILING) { + $trimFn = 'RTRIM'; + } else { + $trimFn = 'TRIM'; + } + + return $trimFn . '(' . $str . $trimChar . ')'; + } + + /** + * return string to call a function to get a substring inside an SQL statement + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function + * + * @param string $value an sql string literal or column name/alias + * @param integer $position where to start the substring portion + * @param integer $length the substring portion length + * @return string SQL substring function with given parameters + * @override + */ + public function getSubstringExpression($value, $position, $length = null) + { + if ($length !== null) { + return 'SUBSTR(' . $value . ', ' . $position . ', ' . $length . ')'; + } + return 'SUBSTR(' . $value . ', ' . $position . ', LENGTH(' . $value . '))'; + } + + /** + * returns the position of the first occurrence of substring $substr in string $str + * + * @param string $substr literal string to find + * @param string $str literal string + * @param int $pos position to start at, beginning of string by default + * @return integer + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos == false) { + return 'LOCATE('.$str.', '.$substr.')'; + } else { + return 'LOCATE('.$str.', '.$substr.', '.$startPos.')'; + } + } + + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case \Doctrine\DBAL\Connection::TRANSACTION_READ_UNCOMMITTED: + return 0; + case \Doctrine\DBAL\Connection::TRANSACTION_READ_COMMITTED: + case \Doctrine\DBAL\Connection::TRANSACTION_REPEATABLE_READ: + case \Doctrine\DBAL\Connection::TRANSACTION_SERIALIZABLE: + return 1; + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + public function getSetTransactionIsolationSQL($level) + { + return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * @override + */ + public function prefersIdentityColumns() + { + return true; + } + + /** + * @override + */ + public function getBooleanTypeDeclarationSQL(array $field) + { + return 'BOOLEAN'; + } + + /** + * @override + */ + public function getIntegerTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getBigIntTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getTinyIntTypeDeclarationSql(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getSmallIntTypeDeclarationSQL(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getMediumIntTypeDeclarationSql(array $field) + { + return $this->_getCommonIntegerTypeDeclarationSQL($field); + } + + /** + * @override + */ + public function getDateTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATETIME'; + } + + /** + * @override + */ + public function getDateTypeDeclarationSQL(array $fieldDeclaration) + { + return 'DATE'; + } + + /** + * @override + */ + public function getTimeTypeDeclarationSQL(array $fieldDeclaration) + { + return 'TIME'; + } + + /** + * @override + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $columnDef) + { + $autoinc = ! empty($columnDef['autoincrement']) ? ' AUTOINCREMENT' : ''; + $pk = ! empty($columnDef['primary']) && ! empty($autoinc) ? ' PRIMARY KEY' : ''; + + return 'INTEGER' . $pk . $autoinc; + } + + /** + * create a new table + * + * @param string $name Name of the database that should be created + * @param array $fields Associative array that contains the definition of each field of the new table + * The indexes of the array entries are the names of the fields of the table an + * the array entry values are associative arrays like those that are meant to be + * passed with the field definitions to get[Type]Declaration() functions. + * array( + * 'id' => array( + * 'type' => 'integer', + * 'unsigned' => 1 + * 'notnull' => 1 + * 'default' => 0 + * ), + * 'name' => array( + * 'type' => 'text', + * 'length' => 12 + * ), + * 'password' => array( + * 'type' => 'text', + * 'length' => 12 + * ) + * ); + * @param array $options An associative array of table options: + * + * @return void + * @override + */ + protected function _getCreateTableSQL($name, array $columns, array $options = array()) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + $autoinc = false; + foreach($columns as $field) { + if (isset($field['autoincrement']) && $field['autoincrement']) { + $autoinc = true; + break; + } + } + + if ( ! $autoinc && isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $keyColumns = array_map(array($this, 'quoteIdentifier'), $keyColumns); + $queryFields.= ', PRIMARY KEY('.implode(', ', $keyColumns).')'; + } + + $query[] = 'CREATE TABLE ' . $name . ' (' . $queryFields . ')'; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index => $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + if (isset($options['unique']) && ! empty($options['unique'])) { + foreach ($options['unique'] as $index => $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + return $query; + } + + /** + * {@inheritdoc} + */ + public function getVarcharTypeDeclarationSQL(array $field) + { + if ( ! isset($field['length'])) { + if (array_key_exists('default', $field)) { + $field['length'] = $this->getVarcharDefaultLength(); + } else { + $field['length'] = false; + } + } + $length = ($field['length'] <= $this->getVarcharMaxLength()) ? $field['length'] : false; + $fixed = (isset($field['fixed'])) ? $field['fixed'] : false; + + return $fixed ? ($length ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length ? 'VARCHAR(' . $length . ')' : 'TEXT'); + } + + public function getClobTypeDeclarationSQL(array $field) + { + return 'CLOB'; + } + + public function getListTableConstraintsSQL($table) + { + return "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = '$table' AND sql NOT NULL ORDER BY name"; + } + + public function getListTableColumnsSQL($table) + { + return "PRAGMA table_info($table)"; + } + + public function getListTableIndexesSQL($table) + { + return "PRAGMA index_list($table)"; + } + + public function getListTablesSQL() + { + return "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' " + . "UNION ALL SELECT name FROM sqlite_temp_master " + . "WHERE type = 'table' ORDER BY name"; + } + + public function getListViewsSQL($database) + { + return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; + } + + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + public function getDropViewSQL($name) + { + return 'DROP VIEW '. $name; + } + + /** + * SQLite does support foreign key constraints, but only in CREATE TABLE statements... + * This really limits their usefulness and requires SQLite specific handling, so + * we simply say that SQLite does NOT support foreign keys for now... + * + * @return boolean FALSE + * @override + */ + public function supportsForeignKeyConstraints() + { + return false; + } + + public function supportsAlterTable() + { + return false; + } + + public function supportsIdentityColumns() + { + return true; + } + + /** + * Get the platform name for this instance + * + * @return string + */ + public function getName() + { + return 'sqlite'; + } + + /** + * @inheritdoc + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + return 'DELETE FROM '.$tableName; + } + + /** + * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction() + * + * @param int|float $value + * @return float + */ + static public function udfSqrt($value) + { + return sqrt($value); + } + + /** + * User-defined function for Sqlite that implements MOD(a, b) + */ + static public function udfMod($a, $b) + { + return ($a % $b); + } + + /** + * @param string $str + * @param string $substr + * @param int $offset + */ + static public function udfLocate($str, $substr, $offset = 0) + { + $pos = strpos($str, $substr, $offset); + if ($pos !== false) { + return $pos+1; + } + return 0; + } + + public function getForUpdateSql() + { + return ''; + } + + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = array( + 'boolean' => 'boolean', + 'tinyint' => 'boolean', + 'smallint' => 'smallint', + 'mediumint' => 'integer', + 'int' => 'integer', + 'integer' => 'integer', + 'serial' => 'integer', + 'bigint' => 'bigint', + 'bigserial' => 'bigint', + 'clob' => 'text', + 'tinytext' => 'text', + 'mediumtext' => 'text', + 'longtext' => 'text', + 'text' => 'text', + 'varchar' => 'string', + 'varchar2' => 'string', + 'nvarchar' => 'string', + 'image' => 'string', + 'ntext' => 'string', + 'char' => 'string', + 'date' => 'date', + 'datetime' => 'datetime', + 'timestamp' => 'datetime', + 'time' => 'time', + 'float' => 'float', + 'double' => 'float', + 'real' => 'float', + 'decimal' => 'decimal', + 'numeric' => 'decimal', + ); + } +} diff --git a/Doctrine/DBAL/README.markdown b/Doctrine/DBAL/README.markdown new file mode 100644 index 0000000..e69de29 diff --git a/Doctrine/DBAL/Schema/AbstractAsset.php b/Doctrine/DBAL/Schema/AbstractAsset.php new file mode 100644 index 0000000..3cef17c --- /dev/null +++ b/Doctrine/DBAL/Schema/AbstractAsset.php @@ -0,0 +1,121 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * The abstract asset allows to reset the name of all assets without publishing this to the public userland. + * + * This encapsulation hack is necessary to keep a consistent state of the database schema. Say we have a list of tables + * array($tableName => Table($tableName)); if you want to rename the table, you have to make sure + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +abstract class AbstractAsset +{ + /** + * @var string + */ + protected $_name; + + protected $_quoted = false; + + /** + * Set name of this asset + * + * @param string $name + */ + protected function _setName($name) + { + if (strlen($name)) { + // TODO: find more elegant way to solve this issue. + if ($name[0] == '`') { + $this->_quoted = true; + $name = trim($name, '`'); + } else if ($name[0] == '"') { + $this->_quoted = true; + $name = trim($name, '"'); + } + } + $this->_name = $name; + } + + /** + * Return name of this schema asset. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Get the quoted representation of this asset but only if it was defined with one. Otherwise + * return the plain unquoted value as inserted. + * + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedName(AbstractPlatform $platform) + { + return ($this->_quoted) ? $platform->quoteIdentifier($this->_name) : $this->_name; + } + + /** + * Generate an identifier from a list of column names obeying a certain string length. + * + * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars, + * however building idents automatically for foreign keys, composite keys or such can easily create + * very long names. + * + * @param array $columnNames + * @param string $postfix + * @param int $maxSize + * @return string + */ + protected function _generateIdentifierName($columnNames, $postfix='', $maxSize=30) + { + $columnCount = count($columnNames); + $postfixLen = strlen($postfix); + $parts = array_map(function($columnName) use($columnCount, $postfixLen, $maxSize) { + return substr($columnName, -floor(($maxSize-$postfixLen)/$columnCount - 1)); + }, $columnNames); + $parts[] = $postfix; + + $identifier = trim(implode("_", $parts), '_'); + // using implicit schema support of DB2 and Postgres there might be dots in the auto-generated + // identifier names which can easily be replaced by underscores. + $identifier = str_replace(".", "_", $identifier); + + if (is_numeric(substr($identifier, 0, 1))) { + $identifier = "i" . substr($identifier, 0, strlen($identifier)-1); + } + + return $identifier; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/AbstractSchemaManager.php b/Doctrine/DBAL/Schema/AbstractSchemaManager.php new file mode 100644 index 0000000..f85dc66 --- /dev/null +++ b/Doctrine/DBAL/Schema/AbstractSchemaManager.php @@ -0,0 +1,777 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Types; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Base class for schema managers. Schema managers are used to inspect and/or + * modify the database schema/structure. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Roman Borschel + * @author Jonathan H. Wage + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +abstract class AbstractSchemaManager +{ + /** + * Holds instance of the Doctrine connection for this schema manager + * + * @var \Doctrine\DBAL\Connection + */ + protected $_conn; + + /** + * Holds instance of the database platform used for this schema manager + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + /** + * Constructor. Accepts the Connection instance to manage the schema for + * + * @param \Doctrine\DBAL\Connection $conn + */ + public function __construct(\Doctrine\DBAL\Connection $conn) + { + $this->_conn = $conn; + $this->_platform = $this->_conn->getDatabasePlatform(); + } + + /** + * Return associated platform. + * + * @return \Doctrine\DBAL\Platform\AbstractPlatform + */ + public function getDatabasePlatform() + { + return $this->_platform; + } + + /** + * Try any method on the schema manager. Normally a method throws an + * exception when your DBMS doesn't support it or if an error occurs. + * This method allows you to try and method on your SchemaManager + * instance and will return false if it does not work or is not supported. + * + * + * $result = $sm->tryMethod('dropView', 'view_name'); + * + * + * @return mixed + */ + public function tryMethod() + { + $args = func_get_args(); + $method = $args[0]; + unset($args[0]); + $args = array_values($args); + + try { + return call_user_func_array(array($this, $method), $args); + } catch (\Exception $e) { + return false; + } + } + + /** + * List the available databases for this connection + * + * @return array $databases + */ + public function listDatabases() + { + $sql = $this->_platform->getListDatabasesSQL(); + + $databases = $this->_conn->fetchAll($sql); + + return $this->_getPortableDatabasesList($databases); + } + + /** + * List the available sequences for this connection + * + * @return Sequence[] + */ + public function listSequences($database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListSequencesSQL($database); + + $sequences = $this->_conn->fetchAll($sql); + + return $this->_getPortableSequencesList($sequences); + } + + /** + * List the columns for a given table. + * + * In contrast to other libraries and to the old version of Doctrine, + * this column definition does try to contain the 'primary' field for + * the reason that it is not portable accross different RDBMS. Use + * {@see listTableIndexes($tableName)} to retrieve the primary key + * of a table. We're a RDBMS specifies more details these are held + * in the platformDetails array. + * + * @param string $table The name of the table. + * @return Column[] + */ + public function listTableColumns($table) + { + $sql = $this->_platform->getListTableColumnsSQL($table); + + $tableColumns = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableColumnList($tableColumns); + } + + /** + * List the indexes for a given table returning an array of Index instances. + * + * Keys of the portable indexes list are all lower-cased. + * + * @param string $table The name of the table + * @return Index[] $tableIndexes + */ + public function listTableIndexes($table) + { + $sql = $this->_platform->getListTableIndexesSQL($table); + + $tableIndexes = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableIndexesList($tableIndexes, $table); + } + + /** + * Return true if all the given tables exist. + * + * @param array $tableNames + * @return bool + */ + public function tablesExist($tableNames) + { + $tableNames = array_map('strtolower', (array)$tableNames); + return count($tableNames) == count(\array_intersect($tableNames, array_map('strtolower', $this->listTableNames()))); + } + + + /** + * Return a list of all tables in the current database + * + * @return array + */ + public function listTableNames() + { + $sql = $this->_platform->getListTablesSQL(); + + $tables = $this->_conn->fetchAll($sql); + + return $this->_getPortableTablesList($tables); + } + + /** + * List the tables for this connection + * + * @return Table[] + */ + public function listTables() + { + $tableNames = $this->listTableNames(); + + $tables = array(); + foreach ($tableNames AS $tableName) { + $tables[] = $this->listTableDetails($tableName); + } + + return $tables; + } + + /** + * @param string $tableName + * @return Table + */ + public function listTableDetails($tableName) + { + $columns = $this->listTableColumns($tableName); + $foreignKeys = array(); + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->listTableForeignKeys($tableName); + } + $indexes = $this->listTableIndexes($tableName); + + return new Table($tableName, $columns, $indexes, $foreignKeys, false, array()); + } + + /** + * List the views this connection has + * + * @return View[] + */ + public function listViews() + { + $database = $this->_conn->getDatabase(); + $sql = $this->_platform->getListViewsSQL($database); + $views = $this->_conn->fetchAll($sql); + + return $this->_getPortableViewsList($views); + } + + /** + * List the foreign keys for the given table + * + * @param string $table The name of the table + * @return ForeignKeyConstraint[] + */ + public function listTableForeignKeys($table, $database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); + $tableForeignKeys = $this->_conn->fetchAll($sql); + + return $this->_getPortableTableForeignKeysList($tableForeignKeys); + } + + /* drop*() Methods */ + + /** + * Drops a database. + * + * NOTE: You can not drop the database this SchemaManager is currently connected to. + * + * @param string $database The name of the database to drop + */ + public function dropDatabase($database) + { + $this->_execSql($this->_platform->getDropDatabaseSQL($database)); + } + + /** + * Drop the given table + * + * @param string $table The name of the table to drop + */ + public function dropTable($table) + { + $this->_execSql($this->_platform->getDropTableSQL($table)); + } + + /** + * Drop the index from the given table + * + * @param Index|string $index The name of the index + * @param string|Table $table The name of the table + */ + public function dropIndex($index, $table) + { + if($index instanceof Index) { + $index = $index->getQuotedName($this->_platform); + } + + $this->_execSql($this->_platform->getDropIndexSQL($index, $table)); + } + + /** + * Drop the constraint from the given table + * + * @param Constraint $constraint + * @param string $table The name of the table + */ + public function dropConstraint(Constraint $constraint, $table) + { + $this->_execSql($this->_platform->getDropConstraintSQL($constraint, $table)); + } + + /** + * Drops a foreign key from a table. + * + * @param ForeignKeyConstraint|string $table The name of the table with the foreign key. + * @param Table|string $name The name of the foreign key. + * @return boolean $result + */ + public function dropForeignKey($foreignKey, $table) + { + $this->_execSql($this->_platform->getDropForeignKeySQL($foreignKey, $table)); + } + + /** + * Drops a sequence with a given name. + * + * @param string $name The name of the sequence to drop. + */ + public function dropSequence($name) + { + $this->_execSql($this->_platform->getDropSequenceSQL($name)); + } + + /** + * Drop a view + * + * @param string $name The name of the view + * @return boolean $result + */ + public function dropView($name) + { + $this->_execSql($this->_platform->getDropViewSQL($name)); + } + + /* create*() Methods */ + + /** + * Creates a new database. + * + * @param string $database The name of the database to create. + */ + public function createDatabase($database) + { + $this->_execSql($this->_platform->getCreateDatabaseSQL($database)); + } + + /** + * Create a new table. + * + * @param Table $table + * @param int $createFlags + */ + public function createTable(Table $table) + { + $createFlags = AbstractPlatform::CREATE_INDEXES|AbstractPlatform::CREATE_FOREIGNKEYS; + $this->_execSql($this->_platform->getCreateTableSQL($table, $createFlags)); + } + + /** + * Create a new sequence + * + * @param Sequence $sequence + * @throws Doctrine\DBAL\ConnectionException if something fails at database level + */ + public function createSequence($sequence) + { + $this->_execSql($this->_platform->getCreateSequenceSQL($sequence)); + } + + /** + * Create a constraint on a table + * + * @param Constraint $constraint + * @param string|Table $table + */ + public function createConstraint(Constraint $constraint, $table) + { + $this->_execSql($this->_platform->getCreateConstraintSQL($constraint, $table)); + } + + /** + * Create a new index on a table + * + * @param Index $index + * @param string $table name of the table on which the index is to be created + */ + public function createIndex(Index $index, $table) + { + $this->_execSql($this->_platform->getCreateIndexSQL($index, $table)); + } + + /** + * Create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey ForeignKey instance + * @param string|Table $table name of the table on which the foreign key is to be created + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $this->_execSql($this->_platform->getCreateForeignKeySQL($foreignKey, $table)); + } + + /** + * Create a new view + * + * @param View $view + */ + public function createView(View $view) + { + $this->_execSql($this->_platform->getCreateViewSQL($view->getQuotedName($this->_platform), $view->getSql())); + } + + /* dropAndCreate*() Methods */ + + /** + * Drop and create a constraint + * + * @param Constraint $constraint + * @param string $table + * @see dropConstraint() + * @see createConstraint() + */ + public function dropAndCreateConstraint(Constraint $constraint, $table) + { + $this->tryMethod('dropConstraint', $constraint, $table); + $this->createConstraint($constraint, $table); + } + + /** + * Drop and create a new index on a table + * + * @param string|Table $table name of the table on which the index is to be created + * @param Index $index + */ + public function dropAndCreateIndex(Index $index, $table) + { + $this->tryMethod('dropIndex', $index->getQuotedName($this->_platform), $table); + $this->createIndex($index, $table); + } + + /** + * Drop and create a new foreign key + * + * @param ForeignKeyConstraint $foreignKey associative array that defines properties of the foreign key to be created. + * @param string|Table $table name of the table on which the foreign key is to be created + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $this->tryMethod('dropForeignKey', $foreignKey, $table); + $this->createForeignKey($foreignKey, $table); + } + + /** + * Drop and create a new sequence + * + * @param Sequence $sequence + * @throws Doctrine\DBAL\ConnectionException if something fails at database level + */ + public function dropAndCreateSequence(Sequence $sequence) + { + $this->tryMethod('createSequence', $seqName, $start, $allocationSize); + $this->createSequence($seqName, $start, $allocationSize); + } + + /** + * Drop and create a new table. + * + * @param Table $table + */ + public function dropAndCreateTable(Table $table) + { + $this->tryMethod('dropTable', $table->getQuotedName($this->_platform)); + $this->createTable($table); + } + + /** + * Drop and creates a new database. + * + * @param string $database The name of the database to create. + */ + public function dropAndCreateDatabase($database) + { + $this->tryMethod('dropDatabase', $database); + $this->createDatabase($database); + } + + /** + * Drop and create a new view + * + * @param View $view + */ + public function dropAndCreateView(View $view) + { + $this->tryMethod('dropView', $view->getQuotedName($this->_platform)); + $this->createView($view); + } + + /* alterTable() Methods */ + + /** + * Alter an existing tables schema + * + * @param TableDiff $tableDiff + */ + public function alterTable(TableDiff $tableDiff) + { + $queries = $this->_platform->getAlterTableSQL($tableDiff); + if (is_array($queries) && count($queries)) { + foreach ($queries AS $ddlQuery) { + $this->_execSql($ddlQuery); + } + } + } + + /** + * Rename a given table to another name + * + * @param string $name The current name of the table + * @param string $newName The new name of the table + */ + public function renameTable($name, $newName) + { + $tableDiff = new TableDiff($name); + $tableDiff->newName = $newName; + $this->alterTable($tableDiff); + } + + /** + * Methods for filtering return values of list*() methods to convert + * the native DBMS data definition to a portable Doctrine definition + */ + + protected function _getPortableDatabasesList($databases) + { + $list = array(); + foreach ($databases as $key => $value) { + if ($value = $this->_getPortableDatabaseDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database; + } + + protected function _getPortableFunctionsList($functions) + { + $list = array(); + foreach ($functions as $key => $value) { + if ($value = $this->_getPortableFunctionDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableFunctionDefinition($function) + { + return $function; + } + + protected function _getPortableTriggersList($triggers) + { + $list = array(); + foreach ($triggers as $key => $value) { + if ($value = $this->_getPortableTriggerDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTriggerDefinition($trigger) + { + return $trigger; + } + + protected function _getPortableSequencesList($sequences) + { + $list = array(); + foreach ($sequences as $key => $value) { + if ($value = $this->_getPortableSequenceDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + /** + * @param array $sequence + * @return Sequence + */ + protected function _getPortableSequenceDefinition($sequence) + { + throw DBALException::notSupported('Sequences'); + } + + /** + * Independent of the database the keys of the column list result are lowercased. + * + * The name of the created column instance however is kept in its case. + * + * @param array $tableColumns + * @return array + */ + protected function _getPortableTableColumnList($tableColumns) + { + $list = array(); + foreach ($tableColumns as $key => $column) { + if ($column = $this->_getPortableTableColumnDefinition($column)) { + $name = strtolower($column->getQuotedName($this->_platform)); + $list[$name] = $column; + } + } + return $list; + } + + /** + * Get Table Column Definition + * + * @param array $tableColumn + * @return Column + */ + abstract protected function _getPortableTableColumnDefinition($tableColumn); + + /** + * Aggregate and group the index results according to the required data result. + * + * @param array $tableIndexRows + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexRows, $tableName=null) + { + $result = array(); + foreach($tableIndexRows AS $tableIndex) { + $indexName = $keyName = $tableIndex['key_name']; + if($tableIndex['primary']) { + $keyName = 'primary'; + } + $keyName = strtolower($keyName); + + if(!isset($result[$keyName])) { + $result[$keyName] = array( + 'name' => $indexName, + 'columns' => array($tableIndex['column_name']), + 'unique' => $tableIndex['non_unique'] ? false : true, + 'primary' => $tableIndex['primary'], + ); + } else { + $result[$keyName]['columns'][] = $tableIndex['column_name']; + } + } + + $indexes = array(); + foreach($result AS $indexKey => $data) { + $indexes[$indexKey] = new Index($data['name'], $data['columns'], $data['unique'], $data['primary']); + } + + return $indexes; + } + + protected function _getPortableTablesList($tables) + { + $list = array(); + foreach ($tables as $key => $value) { + if ($value = $this->_getPortableTableDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTableDefinition($table) + { + return $table; + } + + protected function _getPortableUsersList($users) + { + $list = array(); + foreach ($users as $key => $value) { + if ($value = $this->_getPortableUserDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableUserDefinition($user) + { + return $user; + } + + protected function _getPortableViewsList($views) + { + $list = array(); + foreach ($views as $key => $value) { + if ($view = $this->_getPortableViewDefinition($value)) { + $viewName = strtolower($view->getQuotedName($this->_platform)); + $list[$viewName] = $view; + } + } + return $list; + } + + protected function _getPortableViewDefinition($view) + { + return false; + } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $key => $value) { + if ($value = $this->_getPortableTableForeignKeyDefinition($value)) { + $list[] = $value; + } + } + return $list; + } + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return $tableForeignKey; + } + + protected function _execSql($sql) + { + foreach ((array) $sql as $query) { + $this->_conn->executeUpdate($query); + } + } + + /** + * Create a schema instance for the current database. + * + * @return Schema + */ + public function createSchema() + { + $sequences = array(); + if($this->_platform->supportsSequences()) { + $sequences = $this->listSequences(); + } + $tables = $this->listTables(); + + return new Schema($tables, $sequences, $this->createSchemaConfig()); + } + + /** + * Create the configuration for this schema. + * + * @return SchemaConfig + */ + public function createSchemaConfig() + { + $schemaConfig = new SchemaConfig(); + $schemaConfig->setMaxIdentifierLength($this->_platform->getMaxIdentifierLength()); + + return $schemaConfig; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Column.php b/Doctrine/DBAL/Schema/Column.php new file mode 100644 index 0000000..d31961d --- /dev/null +++ b/Doctrine/DBAL/Schema/Column.php @@ -0,0 +1,346 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use \Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Object representation of a database column + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Column extends AbstractAsset +{ + /** + * @var \Doctrine\DBAL\Types\Type + */ + protected $_type; + + /** + * @var int + */ + protected $_length = null; + + /** + * @var int + */ + protected $_precision = 10; + + /** + * @var int + */ + protected $_scale = 0; + + /** + * @var bool + */ + protected $_unsigned = false; + + /** + * @var bool + */ + protected $_fixed = false; + + /** + * @var bool + */ + protected $_notnull = true; + + /** + * @var string + */ + protected $_default = null; + + /** + * @var bool + */ + protected $_autoincrement = false; + + /** + * @var array + */ + protected $_platformOptions = array(); + + /** + * @var string + */ + protected $_columnDefinition = null; + + /** + * Create a new Column + * + * @param string $columnName + * @param Doctrine\DBAL\Types\Type $type + * @param int $length + * @param bool $notNull + * @param mixed $default + * @param bool $unsigned + * @param bool $fixed + * @param int $precision + * @param int $scale + * @param array $platformOptions + */ + public function __construct($columnName, Type $type, array $options=array()) + { + $this->_setName($columnName); + $this->setType($type); + $this->setOptions($options); + } + + /** + * @param array $options + * @return Column + */ + public function setOptions(array $options) + { + foreach ($options AS $name => $value) { + $method = "set".$name; + if (method_exists($this, $method)) { + $this->$method($value); + } + } + return $this; + } + + /** + * @param Type $type + * @return Column + */ + public function setType(Type $type) + { + $this->_type = $type; + return $this; + } + + /** + * @param int $length + * @return Column + */ + public function setLength($length) + { + if($length !== null) { + $this->_length = (int)$length; + } else { + $this->_length = null; + } + return $this; + } + + /** + * @param int $precision + * @return Column + */ + public function setPrecision($precision) + { + $this->_precision = (int)$precision; + return $this; + } + + /** + * @param int $scale + * @return Column + */ + public function setScale($scale) + { + $this->_scale = $scale; + return $this; + } + + /** + * + * @param bool $unsigned + * @return Column + */ + public function setUnsigned($unsigned) + { + $this->_unsigned = (bool)$unsigned; + return $this; + } + + /** + * + * @param bool $fixed + * @return Column + */ + public function setFixed($fixed) + { + $this->_fixed = (bool)$fixed; + return $this; + } + + /** + * @param bool $notnull + * @return Column + */ + public function setNotnull($notnull) + { + $this->_notnull = (bool)$notnull; + return $this; + } + + /** + * + * @param mixed $default + * @return Column + */ + public function setDefault($default) + { + $this->_default = $default; + return $this; + } + + /** + * + * @param array $platformOptions + * @return Column + */ + public function setPlatformOptions(array $platformOptions) + { + $this->_platformOptions = $platformOptions; + return $this; + } + + /** + * + * @param string $name + * @param mixed $value + * @return Column + */ + public function setPlatformOption($name, $value) + { + $this->_platformOptions[$name] = $value; + return $this; + } + + /** + * + * @param string + * @return Column + */ + public function setColumnDefinition($value) + { + $this->_columnDefinition = $value; + return $this; + } + + public function getType() + { + return $this->_type; + } + + public function getLength() + { + return $this->_length; + } + + public function getPrecision() + { + return $this->_precision; + } + + public function getScale() + { + return $this->_scale; + } + + public function getUnsigned() + { + return $this->_unsigned; + } + + public function getFixed() + { + return $this->_fixed; + } + + public function getNotnull() + { + return $this->_notnull; + } + + public function getDefault() + { + return $this->_default; + } + + public function getPlatformOptions() + { + return $this->_platformOptions; + } + + public function hasPlatformOption($name) + { + return isset($this->_platformOptions[$name]); + } + + public function getPlatformOption($name) + { + return $this->_platformOptions[$name]; + } + + public function getColumnDefinition() + { + return $this->_columnDefinition; + } + + public function getAutoincrement() + { + return $this->_autoincrement; + } + + public function setAutoincrement($flag) + { + $this->_autoincrement = $flag; + return $this; + } + + /** + * @param Visitor $visitor + */ + public function visit(\Doctrine\DBAL\Schema\Visitor $visitor) + { + $visitor->accept($this); + } + + /** + * @return array + */ + public function toArray() + { + return array_merge(array( + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, + 'columnDefinition' => $this->_columnDefinition, + ), $this->_platformOptions); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/ColumnDiff.php b/Doctrine/DBAL/Schema/ColumnDiff.php new file mode 100644 index 0000000..4cf8969 --- /dev/null +++ b/Doctrine/DBAL/Schema/ColumnDiff.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Represent the change of a column + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ColumnDiff +{ + public $oldColumnName; + + /** + * @var Column + */ + public $column; + + /** + * @var array + */ + public $changedProperties = array(); + + public function __construct($oldColumnName, Column $column, array $changedProperties = array()) + { + $this->oldColumnName = $oldColumnName; + $this->column = $column; + $this->changedProperties = $changedProperties; + } + + public function hasChanged($propertyName) + { + return in_array($propertyName, $this->changedProperties); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Comparator.php b/Doctrine/DBAL/Schema/Comparator.php new file mode 100644 index 0000000..8de2f3f --- /dev/null +++ b/Doctrine/DBAL/Schema/Comparator.php @@ -0,0 +1,367 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Compare to Schemas and return an instance of SchemaDiff + * + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Comparator +{ + /** + * @param Schema $fromSchema + * @param Schema $toSchema + * @return SchemaDiff + */ + static public function compareSchemas( Schema $fromSchema, Schema $toSchema ) + { + $c = new self(); + return $c->compare($fromSchema, $toSchema); + } + + /** + * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. + * + * The returned diferences are returned in such a way that they contain the + * operations to change the schema stored in $fromSchema to the schema that is + * stored in $toSchema. + * + * @param Schema $fromSchema + * @param Schema $toSchema + * + * @return SchemaDiff + */ + public function compare(Schema $fromSchema, Schema $toSchema) + { + $diff = new SchemaDiff(); + + $foreignKeysToTable = array(); + + foreach ( $toSchema->getTables() AS $tableName => $table ) { + if ( !$fromSchema->hasTable($tableName) ) { + $diff->newTables[$tableName] = $table; + } else { + $tableDifferences = $this->diffTable( $fromSchema->getTable($tableName), $table ); + if ( $tableDifferences !== false ) { + $diff->changedTables[$tableName] = $tableDifferences; + } + } + } + + /* Check if there are tables removed */ + foreach ( $fromSchema->getTables() AS $tableName => $table ) { + if ( !$toSchema->hasTable($tableName) ) { + $diff->removedTables[$tableName] = $table; + } + + // also remember all foreign keys that point to a specific table + foreach ($table->getForeignKeys() AS $foreignKey) { + $foreignTable = strtolower($foreignKey->getForeignTableName()); + if (!isset($foreignKeysToTable[$foreignTable])) { + $foreignKeysToTable[$foreignTable] = array(); + } + $foreignKeysToTable[$foreignTable][] = $foreignKey; + } + } + + foreach ($diff->removedTables AS $tableName => $table) { + if (isset($foreignKeysToTable[$tableName])) { + $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]); + } + } + + foreach ( $toSchema->getSequences() AS $sequenceName => $sequence) { + if (!$fromSchema->hasSequence($sequenceName)) { + $diff->newSequences[] = $sequence; + } else { + if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { + $diff->changedSequences[] = $fromSchema->getSequence($sequenceName); + } + } + } + + foreach ($fromSchema->getSequences() AS $sequenceName => $sequence) { + if (!$toSchema->hasSequence($sequenceName)) { + $diff->removedSequences[] = $sequence; + } + } + + return $diff; + } + + /** + * + * @param Sequence $sequence1 + * @param Sequence $sequence2 + */ + public function diffSequence(Sequence $sequence1, Sequence $sequence2) + { + if($sequence1->getAllocationSize() != $sequence2->getAllocationSize()) { + return true; + } + + if($sequence1->getInitialValue() != $sequence2->getInitialValue()) { + return true; + } + + return false; + } + + /** + * Returns the difference between the tables $table1 and $table2. + * + * If there are no differences this method returns the boolean false. + * + * @param Table $table1 + * @param Table $table2 + * + * @return bool|TableDiff + */ + public function diffTable(Table $table1, Table $table2) + { + $changes = 0; + $tableDifferences = new TableDiff($table1->getName()); + + $table1Columns = $table1->getColumns(); + $table2Columns = $table2->getColumns(); + + /* See if all the fields in table 1 exist in table 2 */ + foreach ( $table2Columns as $columnName => $column ) { + if ( !$table1->hasColumn($columnName) ) { + $tableDifferences->addedColumns[$columnName] = $column; + $changes++; + } + } + /* See if there are any removed fields in table 2 */ + foreach ( $table1Columns as $columnName => $column ) { + if ( !$table2->hasColumn($columnName) ) { + $tableDifferences->removedColumns[$columnName] = $column; + $changes++; + } + } + foreach ( $table1Columns as $columnName => $column ) { + if ( $table2->hasColumn($columnName) ) { + $changedProperties = $this->diffColumn( $column, $table2->getColumn($columnName) ); + if (count($changedProperties) ) { + $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); + $tableDifferences->changedColumns[$column->getName()] = $columnDiff; + $changes++; + } + } + } + + $this->detectColumnRenamings($tableDifferences); + + $table1Indexes = $table1->getIndexes(); + $table2Indexes = $table2->getIndexes(); + + foreach ($table2Indexes AS $index2Name => $index2Definition) { + foreach ($table1Indexes AS $index1Name => $index1Definition) { + if ($this->diffIndex($index1Definition, $index2Definition) === false) { + unset($table1Indexes[$index1Name]); + unset($table2Indexes[$index2Name]); + } else { + if ($index1Name == $index2Name) { + $tableDifferences->changedIndexes[$index2Name] = $table2Indexes[$index2Name]; + unset($table1Indexes[$index1Name]); + unset($table2Indexes[$index2Name]); + $changes++; + } + } + } + } + + foreach ($table1Indexes AS $index1Name => $index1Definition) { + $tableDifferences->removedIndexes[$index1Name] = $index1Definition; + $changes++; + } + + foreach ($table2Indexes AS $index2Name => $index2Definition) { + $tableDifferences->addedIndexes[$index2Name] = $index2Definition; + $changes++; + } + + $fromFkeys = $table1->getForeignKeys(); + $toFkeys = $table2->getForeignKeys(); + + foreach ($fromFkeys AS $key1 => $constraint1) { + foreach ($toFkeys AS $key2 => $constraint2) { + if($this->diffForeignKey($constraint1, $constraint2) === false) { + unset($fromFkeys[$key1]); + unset($toFkeys[$key2]); + } else { + if (strtolower($constraint1->getName()) == strtolower($constraint2->getName())) { + $tableDifferences->changedForeignKeys[] = $constraint2; + $changes++; + unset($fromFkeys[$key1]); + unset($toFkeys[$key2]); + } + } + } + } + + foreach ($fromFkeys AS $key1 => $constraint1) { + $tableDifferences->removedForeignKeys[] = $constraint1; + $changes++; + } + + foreach ($toFkeys AS $key2 => $constraint2) { + $tableDifferences->addedForeignKeys[] = $constraint2; + $changes++; + } + + return $changes ? $tableDifferences : false; + } + + /** + * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguouties between different possibilites should not lead to renaming at all. + * + * @param TableDiff $tableDifferences + */ + private function detectColumnRenamings(TableDiff $tableDifferences) + { + $renameCandidates = array(); + foreach ($tableDifferences->addedColumns AS $addedColumnName => $addedColumn) { + foreach ($tableDifferences->removedColumns AS $removedColumnName => $removedColumn) { + if (count($this->diffColumn($addedColumn, $removedColumn)) == 0) { + $renameCandidates[$addedColumn->getName()][] = array($removedColumn, $addedColumn); + } + } + } + + foreach ($renameCandidates AS $candidate => $candidateColumns) { + if (count($candidateColumns) == 1) { + list($removedColumn, $addedColumn) = $candidateColumns[0]; + + $tableDifferences->renamedColumns[$removedColumn->getName()] = $addedColumn; + unset($tableDifferences->addedColumns[$addedColumnName]); + unset($tableDifferences->removedColumns[$removedColumnName]); + } + } + } + + /** + * @param ForeignKeyConstraint $key1 + * @param ForeignKeyConstraint $key2 + * @return bool + */ + public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) + { + if (array_map('strtolower', $key1->getLocalColumns()) != array_map('strtolower', $key2->getLocalColumns())) { + return true; + } + + if (array_map('strtolower', $key1->getForeignColumns()) != array_map('strtolower', $key2->getForeignColumns())) { + return true; + } + + if ($key1->onUpdate() != $key2->onUpdate()) { + return true; + } + + if ($key1->onDelete() != $key2->onDelete()) { + return true; + } + + return false; + } + + /** + * Returns the difference between the fields $field1 and $field2. + * + * If there are differences this method returns $field2, otherwise the + * boolean false. + * + * @param Column $column1 + * @param Column $column2 + * + * @return array + */ + public function diffColumn(Column $column1, Column $column2) + { + $changedProperties = array(); + if ( $column1->getType() != $column2->getType() ) { + $changedProperties[] = 'type'; + } + + if ($column1->getNotnull() != $column2->getNotnull()) { + $changedProperties[] = 'notnull'; + } + + if ($column1->getDefault() != $column2->getDefault()) { + $changedProperties[] = 'default'; + } + + if ($column1->getUnsigned() != $column2->getUnsigned()) { + $changedProperties[] = 'unsigned'; + } + + if ($column1->getType() instanceof \Doctrine\DBAL\Types\StringType) { + if ($column1->getLength() != $column2->getLength()) { + $changedProperties[] = 'length'; + } + + if ($column1->getFixed() != $column2->getFixed()) { + $changedProperties[] = 'fixed'; + } + } + + if ($column1->getType() instanceof \Doctrine\DBAL\Types\DecimalType) { + if ($column1->getPrecision() != $column2->getPrecision()) { + $changedProperties[] = 'precision'; + } + if ($column1->getScale() != $column2->getScale()) { + $changedProperties[] = 'scale'; + } + } + + if ($column1->getAutoincrement() != $column2->getAutoincrement()) { + $changedProperties[] = 'autoincrement'; + } + + return $changedProperties; + } + + /** + * Finds the difference between the indexes $index1 and $index2. + * + * Compares $index1 with $index2 and returns $index2 if there are any + * differences or false in case there are no differences. + * + * @param Index $index1 + * @param Index $index2 + * @return bool + */ + public function diffIndex(Index $index1, Index $index2) + { + if ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1)) { + return false; + } + return true; + } +} diff --git a/Doctrine/DBAL/Schema/Constraint.php b/Doctrine/DBAL/Schema/Constraint.php new file mode 100644 index 0000000..9e760ff --- /dev/null +++ b/Doctrine/DBAL/Schema/Constraint.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Marker interface for contraints + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +interface Constraint +{ + public function getName(); + + public function getColumns(); +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/DB2SchemaManager.php b/Doctrine/DBAL/Schema/DB2SchemaManager.php new file mode 100644 index 0000000..280647b --- /dev/null +++ b/Doctrine/DBAL/Schema/DB2SchemaManager.php @@ -0,0 +1,186 @@ +. +*/ + +namespace Doctrine\DBAL\Schema; + +/** + * IBM Db2 Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class DB2SchemaManager extends AbstractSchemaManager +{ + /** + * Return a list of all tables in the current database + * + * Apparently creator is the schema not the user who created it: + * {@link http://publib.boulder.ibm.com/infocenter/dzichelp/v2r2/index.jsp?topic=/com.ibm.db29.doc.sqlref/db2z_sysibmsystablestable.htm} + * + * @return array + */ + public function listTableNames() + { + $sql = $this->_platform->getListTablesSQL(); + $sql .= " AND CREATOR = UPPER('".$this->_conn->getUsername()."')"; + + $tables = $this->_conn->fetchAll($sql); + + return $this->_getPortableTablesList($tables); + } + + + /** + * Get Table Column Definition + * + * @param array $tableColumn + * @return Column + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, \CASE_LOWER); + + $length = null; + $fixed = null; + $unsigned = false; + $scale = false; + $precision = false; + + $type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']); + + switch (strtolower($tableColumn['typename'])) { + case 'varchar': + $length = $tableColumn['length']; + $fixed = false; + break; + case 'character': + $length = $tableColumn['length']; + $fixed = true; + break; + case 'clob': + $length = $tableColumn['length']; + break; + case 'decimal': + case 'double': + case 'real': + $scale = $tableColumn['scale']; + $precision = $tableColumn['length']; + break; + } + + $options = array( + 'length' => $length, + 'unsigned' => (bool)$unsigned, + 'fixed' => (bool)$fixed, + 'default' => ($tableColumn['default'] == "NULL") ? null : $tableColumn['default'], + 'notnull' => (bool) ($tableColumn['nulls'] == 'N'), + 'scale' => null, + 'precision' => null, + 'platformOptions' => array(), + ); + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['colname'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableTablesList($tables) + { + $tableNames = array(); + foreach ($tables AS $tableRow) { + $tableRow = array_change_key_case($tableRow, \CASE_LOWER); + $tableNames[] = $tableRow['name']; + } + return $tableNames; + } + + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $tableIndexRows = array(); + $indexes = array(); + foreach($tableIndexes AS $indexKey => $data) { + $data = array_change_key_case($data, \CASE_LOWER); + $unique = ($data['uniquerule'] == "D") ? false : true; + $primary = ($data['uniquerule'] == "P"); + + $indexName = strtolower($data['name']); + if ($primary) { + $keyName = 'primary'; + } else { + $keyName = $indexName; + } + + $indexes[$keyName] = new Index($indexName, explode("+", ltrim($data['colnames'], '+')), $unique, $primary); + } + + return $indexes; + } + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); + + $tableForeignKey['deleterule'] = $this->_getPortableForeignKeyRuleDef($tableForeignKey['deleterule']); + $tableForeignKey['updaterule'] = $this->_getPortableForeignKeyRuleDef($tableForeignKey['updaterule']); + + return new ForeignKeyConstraint( + array_map('trim', (array)$tableForeignKey['fkcolnames']), + $tableForeignKey['reftbname'], + array_map('trim', (array)$tableForeignKey['pkcolnames']), + $tableForeignKey['relname'], + array( + 'onUpdate' => $tableForeignKey['updaterule'], + 'onDelete' => $tableForeignKey['deleterule'], + ) + ); + } + + protected function _getPortableForeignKeyRuleDef($def) + { + if ($def == "C") { + return "CASCADE"; + } else if ($def == "N") { + return "SET NULL"; + } + return null; + } + + protected function _getPortableViewDefinition($view) + { + $view = array_change_key_case($view, \CASE_LOWER); + // sadly this still segfaults on PDO_IBM, see http://pecl.php.net/bugs/bug.php?id=17199 + //$view['text'] = (is_resource($view['text']) ? stream_get_contents($view['text']) : $view['text']); + if (!is_resource($view['text'])) { + $pos = strpos($view['text'], ' AS '); + $sql = substr($view['text'], $pos+4); + } else { + $sql = ''; + } + + return new View($view['name'], $sql); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/ForeignKeyConstraint.php b/Doctrine/DBAL/Schema/ForeignKeyConstraint.php new file mode 100644 index 0000000..398c727 --- /dev/null +++ b/Doctrine/DBAL/Schema/ForeignKeyConstraint.php @@ -0,0 +1,164 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; + +class ForeignKeyConstraint extends AbstractAsset implements Constraint +{ + /** + * @var Table + */ + protected $_localTable; + + /** + * @var array + */ + protected $_localColumnNames; + + /** + * @var string + */ + protected $_foreignTableName; + + /** + * @var array + */ + protected $_foreignColumnNames; + + /** + * @var string + */ + protected $_cascade = ''; + + /** + * @var array + */ + protected $_options; + + /** + * + * @param array $localColumnNames + * @param string $foreignTableName + * @param array $foreignColumnNames + * @param string $cascade + * @param string|null $name + */ + public function __construct(array $localColumnNames, $foreignTableName, array $foreignColumnNames, $name=null, array $options=array()) + { + $this->_setName($name); + $this->_localColumnNames = $localColumnNames; + $this->_foreignTableName = $foreignTableName; + $this->_foreignColumnNames = $foreignColumnNames; + $this->_options = $options; + } + + /** + * @return string + */ + public function getLocalTableName() + { + return $this->_localTable->getName(); + } + + /** + * @param Table $table + */ + public function setLocalTable(Table $table) + { + $this->_localTable = $table; + } + + /** + * @return array + */ + public function getLocalColumns() + { + return $this->_localColumnNames; + } + + public function getColumns() + { + return $this->_localColumnNames; + } + + /** + * @return string + */ + public function getForeignTableName() + { + return $this->_foreignTableName; + } + + /** + * @return array + */ + public function getForeignColumns() + { + return $this->_foreignColumnNames; + } + + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + public function getOption($name) + { + return $this->_options[$name]; + } + + /** + * Foreign Key onUpdate status + * + * @return string|null + */ + public function onUpdate() + { + return $this->_onEvent('onUpdate'); + } + + /** + * Foreign Key onDelete status + * + * @return string|null + */ + public function onDelete() + { + return $this->_onEvent('onDelete'); + } + + /** + * @param string $event + * @return string|null + */ + private function _onEvent($event) + { + if (isset($this->_options[$event])) { + $onEvent = strtoupper($this->_options[$event]); + if (!in_array($onEvent, array('NO ACTION', 'RESTRICT'))) { + return $onEvent; + } + } + return false; + } +} diff --git a/Doctrine/DBAL/Schema/Index.php b/Doctrine/DBAL/Schema/Index.php new file mode 100644 index 0000000..50f045e --- /dev/null +++ b/Doctrine/DBAL/Schema/Index.php @@ -0,0 +1,176 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; + +class Index extends AbstractAsset implements Constraint +{ + /** + * @var array + */ + protected $_columns; + + /** + * @var bool + */ + protected $_isUnique = false; + + /** + * @var bool + */ + protected $_isPrimary = false; + + /** + * @param string $indexName + * @param array $column + * @param bool $isUnique + * @param bool $isPrimary + */ + public function __construct($indexName, array $columns, $isUnique=false, $isPrimary=false) + { + $isUnique = ($isPrimary)?true:$isUnique; + + $this->_setName($indexName); + $this->_isUnique = $isUnique; + $this->_isPrimary = $isPrimary; + + foreach($columns AS $column) { + $this->_addColumn($column); + } + } + + /** + * @param string $column + */ + protected function _addColumn($column) + { + if(is_string($column)) { + $this->_columns[] = $column; + } else { + throw new \InvalidArgumentException("Expecting a string as Index Column"); + } + } + + /** + * @return array + */ + public function getColumns() + { + return $this->_columns; + } + + /** + * @return bool + */ + public function isUnique() + { + return $this->_isUnique; + } + + /** + * @return bool + */ + public function isPrimary() + { + return $this->_isPrimary; + } + + /** + * @param string $columnName + * @param int $pos + * @return bool + */ + public function hasColumnAtPosition($columnName, $pos=0) + { + $columnName = strtolower($columnName); + $indexColumns = \array_map('strtolower', $this->getColumns()); + return \array_search($columnName, $indexColumns) === $pos; + } + + /** + * Check if this index exactly spans the given column names in the correct order. + * + * @param array $columnNames + * @return boolean + */ + public function spansColumns(array $columnNames) + { + $sameColumns = true; + for ($i = 0; $i < count($this->_columns); $i++) { + if (!isset($columnNames[$i]) || strtolower($this->_columns[$i]) != strtolower($columnNames[$i])) { + $sameColumns = false; + } + } + return $sameColumns; + } + + /** + * Check if the other index already fullfills all the indexing and constraint needs of the current one. + * + * @param Index $other + * @return bool + */ + public function isFullfilledBy(Index $other) + { + // allow the other index to be equally large only. It being larger is an option + // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) + if (count($other->getColumns()) != count($this->getColumns())) { + return false; + } + + // Check if columns are the same, and even in the same order + $sameColumns = $this->spansColumns($other->getColumns()); + + if ($sameColumns) { + if (!$this->isUnique() && !$this->isPrimary()) { + // this is a special case: If the current key is neither primary or unique, any uniqe or + // primary key will always have the same effect for the index and there cannot be any constraint + // overlaps. This means a primary or unique index can always fullfill the requirements of just an + // index that has no constraints. + return true; + } else if ($other->isPrimary() != $this->isPrimary()) { + return false; + } else if ($other->isUnique() != $this->isUnique()) { + return false; + } + return true; + } + return false; + } + + /** + * Detect if the other index is a non-unique, non primary index that can be overwritten by this one. + * + * @param Index $other + * @return bool + */ + public function overrules(Index $other) + { + if ($other->isPrimary() || $other->isUnique()) { + return false; + } + + if ($this->spansColumns($other->getColumns()) && ($this->isPrimary() || $this->isUnique())) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/MsSqlSchemaManager.php b/Doctrine/DBAL/Schema/MsSqlSchemaManager.php new file mode 100644 index 0000000..b09518a --- /dev/null +++ b/Doctrine/DBAL/Schema/MsSqlSchemaManager.php @@ -0,0 +1,172 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * xxx + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Juozas Kaziukenas + * @version $Revision$ + * @since 2.0 + */ +class MsSqlSchemaManager extends AbstractSchemaManager +{ + + /** + * @override + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $dbType = strtolower($tableColumn['TYPE_NAME']); + + $autoincrement = false; + if (stripos($dbType, 'identity')) { + $dbType = trim(str_ireplace('identity', '', $dbType)); + $autoincrement = true; + } + + $type = array(); + $unsigned = $fixed = null; + + if (!isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $default = $tableColumn['COLUMN_DEF']; + + while ($default != ($default2 = preg_replace("/^\((.*)\)$/", '$1', $default))) { + $default = $default2; + } + + $length = (int) $tableColumn['LENGTH']; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + switch ($type) { + case 'char': + if ($tableColumn['LENGTH'] == '1') { + $type = 'boolean'; + if (preg_match('/^(is|has)/', $tableColumn['name'])) { + $type = array_reverse($type); + } + } + $fixed = true; + break; + case 'text': + $fixed = false; + break; + } + switch ($dbType) { + case 'nchar': + case 'nvarchar': + case 'ntext': + // Unicode data requires 2 bytes per character + $length = $length / 2; + break; + } + + $options = array( + 'length' => ($length == 0 || !in_array($type, array('text', 'string'))) ? null : $length, + 'unsigned' => (bool) $unsigned, + 'fixed' => (bool) $fixed, + 'default' => $default !== 'NULL' ? $default : null, + 'notnull' => (bool) ($tableColumn['IS_NULLABLE'] != 'YES'), + 'scale' => $tableColumn['SCALE'], + 'precision' => $tableColumn['PRECISION'], + 'autoincrement' => $autoincrement, + ); + + return new Column($tableColumn['COLUMN_NAME'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + /** + * @override + */ + protected function _getPortableTableIndexesList($tableIndexRows, $tableName=null) + { + $result = array(); + foreach ($tableIndexRows AS $tableIndex) { + $indexName = $keyName = $tableIndex['index_name']; + if (strpos($tableIndex['index_description'], 'primary key') !== false) { + $keyName = 'primary'; + } + $keyName = strtolower($keyName); + + $result[$keyName] = array( + 'name' => $indexName, + 'columns' => explode(', ', $tableIndex['index_keys']), + 'unique' => strpos($tableIndex['index_description'], 'unique') !== false, + 'primary' => strpos($tableIndex['index_description'], 'primary key') !== false, + ); + } + + $indexes = array(); + foreach ($result AS $indexKey => $data) { + $indexes[$indexKey] = new Index($data['name'], $data['columns'], $data['unique'], $data['primary']); + } + + return $indexes; + } + + /** + * @override + */ + public function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return new ForeignKeyConstraint( + (array) $tableForeignKey['ColumnName'], + $tableForeignKey['ReferenceTableName'], + (array) $tableForeignKey['ReferenceColumnName'], + $tableForeignKey['ForeignKey'], + array( + 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), + 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), + ) + ); + } + + /** + * @override + */ + protected function _getPortableTableDefinition($table) + { + return $table['name']; + } + + /** + * @override + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database['name']; + } + + /** + * @override + */ + protected function _getPortableViewDefinition($view) + { + // @todo + return new View($view['name'], null); + } + +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/MySqlSchemaManager.php b/Doctrine/DBAL/Schema/MySqlSchemaManager.php new file mode 100644 index 0000000..d7be73c --- /dev/null +++ b/Doctrine/DBAL/Schema/MySqlSchemaManager.php @@ -0,0 +1,191 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Schema manager for the MySql RDBMS. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Roman Borschel + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +class MySqlSchemaManager extends AbstractSchemaManager +{ + protected function _getPortableViewDefinition($view) + { + return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); + } + + protected function _getPortableTableDefinition($table) + { + return array_shift($table); + } + + protected function _getPortableUserDefinition($user) + { + return array( + 'user' => $user['User'], + 'password' => $user['Password'], + ); + } + + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + foreach($tableIndexes AS $k => $v) { + $v = array_change_key_case($v, CASE_LOWER); + if($v['key_name'] == 'PRIMARY') { + $v['primary'] = true; + } else { + $v['primary'] = false; + } + $tableIndexes[$k] = $v; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + protected function _getPortableSequenceDefinition($sequence) + { + return end($sequence); + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database['Database']; + } + + /** + * Gets a portable column definition. + * + * The database type is mapped to a corresponding Doctrine mapping type. + * + * @param $tableColumn + * @return array + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['type']); + $dbType = strtok($dbType, '(), '); + if (isset($tableColumn['length'])) { + $length = $tableColumn['length']; + $decimal = ''; + } else { + $length = strtok('(), '); + $decimal = strtok('(), ') ? strtok('(), '):null; + } + $type = array(); + $unsigned = $fixed = null; + + if ( ! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $scale = null; + $precision = null; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + switch ($dbType) { + case 'char': + $fixed = true; + break; + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + if(preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['type'], $match)) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + break; + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + case 'bigint': + case 'tinyblob': + case 'mediumblob': + case 'longblob': + case 'blob': + case 'binary': + case 'varbinary': + case 'year': + $length = null; + break; + } + + $length = ((int) $length == 0) ? null : (int) $length; + $def = array( + 'type' => $type, + 'length' => $length, + 'unsigned' => (bool) $unsigned, + 'fixed' => (bool) $fixed + ); + + $options = array( + 'length' => $length, + 'unsigned' => (bool)$unsigned, + 'fixed' => (bool)$fixed, + 'default' => $tableColumn['default'], + 'notnull' => (bool) ($tableColumn['null'] != 'YES'), + 'scale' => null, + 'precision' => null, + 'autoincrement' => (bool) (strpos($tableColumn['extra'], 'auto_increment') !== false), + ); + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['field'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + public function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); + + if (!isset($tableForeignKey['delete_rule']) || $tableForeignKey['delete_rule'] == "RESTRICT") { + $tableForeignKey['delete_rule'] = null; + } + if (!isset($tableForeignKey['update_rule']) || $tableForeignKey['update_rule'] == "RESTRICT") { + $tableForeignKey['update_rule'] = null; + } + + return new ForeignKeyConstraint( + (array)$tableForeignKey['column_name'], + $tableForeignKey['referenced_table_name'], + (array)$tableForeignKey['referenced_column_name'], + $tableForeignKey['constraint_name'], + array( + 'onUpdate' => $tableForeignKey['update_rule'], + 'onDelete' => $tableForeignKey['delete_rule'], + ) + ); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/OracleSchemaManager.php b/Doctrine/DBAL/Schema/OracleSchemaManager.php new file mode 100644 index 0000000..1849313 --- /dev/null +++ b/Doctrine/DBAL/Schema/OracleSchemaManager.php @@ -0,0 +1,280 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Oracle Schema Manager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +class OracleSchemaManager extends AbstractSchemaManager +{ + protected function _getPortableViewDefinition($view) + { + $view = \array_change_key_case($view, CASE_LOWER); + + return new View($view['view_name'], $view['text']); + } + + protected function _getPortableUserDefinition($user) + { + $user = \array_change_key_case($user, CASE_LOWER); + + return array( + 'user' => $user['username'], + ); + } + + protected function _getPortableTableDefinition($table) + { + $table = \array_change_key_case($table, CASE_LOWER); + + return $table['table_name']; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $indexBuffer = array(); + foreach ( $tableIndexes as $tableIndex ) { + $tableIndex = \array_change_key_case($tableIndex, CASE_LOWER); + + $keyName = strtolower($tableIndex['name']); + + if ( strtolower($tableIndex['is_primary']) == "p" ) { + $keyName = 'primary'; + $buffer['primary'] = true; + $buffer['non_unique'] = false; + } else { + $buffer['primary'] = false; + $buffer['non_unique'] = ( $tableIndex['is_unique'] == 0 ) ? true : false; + } + $buffer['key_name'] = $keyName; + $buffer['column_name'] = $tableIndex['column_name']; + $indexBuffer[] = $buffer; + } + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = \array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['data_type']); + if(strpos($dbType, "timestamp(") === 0) { + if (strpos($dbType, "WITH TIME ZONE")) { + $dbType = "timestamptz"; + } else { + $dbType = "timestamp"; + } + } + + $type = array(); + $length = $unsigned = $fixed = null; + if ( ! empty($tableColumn['data_length'])) { + $length = $tableColumn['data_length']; + } + + if ( ! isset($tableColumn['column_name'])) { + $tableColumn['column_name'] = ''; + } + + if (stripos($tableColumn['data_default'], 'NULL') !== null) { + $tableColumn['data_default'] = null; + } + + $precision = null; + $scale = null; + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + switch ($dbType) { + case 'number': + if ($tableColumn['data_precision'] == 20 && $tableColumn['data_scale'] == 0) { + $precision = 20; + $scale = 0; + $type = 'bigint'; + } elseif ($tableColumn['data_precision'] == 5 && $tableColumn['data_scale'] == 0) { + $type = 'smallint'; + $precision = 5; + $scale = 0; + } elseif ($tableColumn['data_precision'] == 1 && $tableColumn['data_scale'] == 0) { + $precision = 1; + $scale = 0; + $type = 'boolean'; + } elseif ($tableColumn['data_scale'] > 0) { + $precision = $tableColumn['data_precision']; + $scale = $tableColumn['data_scale']; + $type = 'decimal'; + } + $length = null; + break; + case 'pls_integer': + case 'binary_integer': + $length = null; + break; + case 'varchar': + case 'varchar2': + case 'nvarchar2': + $fixed = false; + break; + case 'char': + case 'nchar': + $fixed = true; + break; + case 'date': + case 'timestamp': + $length = null; + break; + case 'float': + $precision = $tableColumn['data_precision']; + $scale = $tableColumn['data_scale']; + $length = null; + break; + case 'clob': + case 'nclob': + $length = null; + break; + case 'blob': + case 'raw': + case 'long raw': + case 'bfile': + $length = null; + break; + case 'rowid': + case 'urowid': + default: + $length = null; + } + + $options = array( + 'notnull' => (bool) ($tableColumn['nullable'] === 'N'), + 'fixed' => (bool) $fixed, + 'unsigned' => (bool) $unsigned, + 'default' => $tableColumn['data_default'], + 'length' => $length, + 'precision' => $precision, + 'scale' => $scale, + 'platformDetails' => array(), + ); + + return new Column($tableColumn['column_name'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = array(); + foreach ($tableForeignKeys as $key => $value) { + $value = \array_change_key_case($value, CASE_LOWER); + if (!isset($list[$value['constraint_name']])) { + if ($value['delete_rule'] == "NO ACTION") { + $value['delete_rule'] = null; + } + + $list[$value['constraint_name']] = array( + 'name' => $value['constraint_name'], + 'local' => array(), + 'foreign' => array(), + 'foreignTable' => $value['references_table'], + 'onDelete' => $value['delete_rule'], + ); + } + $list[$value['constraint_name']]['local'][$value['position']] = $value['local_column']; + $list[$value['constraint_name']]['foreign'][$value['position']] = $value['foreign_column']; + } + + $result = array(); + foreach($list AS $constraint) { + $result[] = new ForeignKeyConstraint( + array_values($constraint['local']), $constraint['foreignTable'], + array_values($constraint['foreign']), $constraint['name'], + array('onDelete' => $constraint['onDelete']) + ); + } + + return $result; + } + + protected function _getPortableSequenceDefinition($sequence) + { + $sequence = \array_change_key_case($sequence, CASE_LOWER); + return new Sequence($sequence['sequence_name'], $sequence['increment_by'], $sequence['min_value']); + } + + protected function _getPortableFunctionDefinition($function) + { + $function = \array_change_key_case($function, CASE_LOWER); + return $function['name']; + } + + protected function _getPortableDatabaseDefinition($database) + { + $database = \array_change_key_case($database, CASE_LOWER); + return $database['username']; + } + + public function createDatabase($database = null) + { + if (is_null($database)) { + $database = $this->_conn->getDatabase(); + } + + $params = $this->_conn->getParams(); + $username = $database; + $password = $params['password']; + + $query = 'CREATE USER ' . $username . ' IDENTIFIED BY ' . $password; + $result = $this->_conn->executeUpdate($query); + + $query = 'GRANT CREATE SESSION, CREATE TABLE, UNLIMITED TABLESPACE, CREATE SEQUENCE, CREATE TRIGGER TO ' . $username; + $result = $this->_conn->executeUpdate($query); + + return true; + } + + public function dropAutoincrement($table) + { + $sql = $this->_platform->getDropAutoincrementSql($table); + foreach ($sql as $query) { + $this->_conn->executeUpdate($query); + } + + return true; + } + + public function dropTable($name) + { + $this->dropAutoincrement($name); + + return parent::dropTable($name); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php b/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php new file mode 100644 index 0000000..f33536a --- /dev/null +++ b/Doctrine/DBAL/Schema/PostgreSqlSchemaManager.php @@ -0,0 +1,275 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * xxx + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Benjamin Eberlei + * @version $Revision$ + * @since 2.0 + */ +class PostgreSqlSchemaManager extends AbstractSchemaManager +{ + + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $onUpdate = null; + $onDelete = null; + + if (preg_match('(ON UPDATE ([a-zA-Z0-9]+))', $tableForeignKey['condef'], $match)) { + $onUpdate = $match[1]; + } + if (preg_match('(ON DELETE ([a-zA-Z0-9]+))', $tableForeignKey['condef'], $match)) { + $onDelete = $match[1]; + } + + if (preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values)) { + $localColumns = array_map('trim', explode(",", $values[1])); + $foreignColumns = array_map('trim', explode(",", $values[3])); + $foreignTable = $values[2]; + } + + return new ForeignKeyConstraint( + $localColumns, $foreignTable, $foreignColumns, $tableForeignKey['conname'], + array('onUpdate' => $onUpdate, 'onDelete' => $onDelete) + ); + } + + public function dropDatabase($database) + { + $params = $this->_conn->getParams(); + $params["dbname"] = "postgres"; + $tmpPlatform = $this->_platform; + $tmpConn = $this->_conn; + + $this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params); + $this->_platform = $this->_conn->getDatabasePlatform(); + + parent::dropDatabase($database); + + $this->_platform = $tmpPlatform; + $this->_conn = $tmpConn; + } + + public function createDatabase($database) + { + $params = $this->_conn->getParams(); + $params["dbname"] = "postgres"; + $tmpPlatform = $this->_platform; + $tmpConn = $this->_conn; + + $this->_conn = \Doctrine\DBAL\DriverManager::getConnection($params); + $this->_platform = $this->_conn->getDatabasePlatform(); + + parent::createDatabase($database); + + $this->_platform = $tmpPlatform; + $this->_conn = $tmpConn; + } + + protected function _getPortableTriggerDefinition($trigger) + { + return $trigger['trigger_name']; + } + + protected function _getPortableViewDefinition($view) + { + return new View($view['viewname'], $view['definition']); + } + + protected function _getPortableUserDefinition($user) + { + return array( + 'user' => $user['usename'], + 'password' => $user['passwd'] + ); + } + + protected function _getPortableTableDefinition($table) + { + return $table['table_name']; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $buffer = array(); + foreach ($tableIndexes AS $row) { + $colNumbers = explode(' ', $row['indkey']); + $colNumbersSql = 'IN (' . join(' ,', $colNumbers) . ' )'; + $columnNameSql = "SELECT attnum, attname FROM pg_attribute + WHERE attrelid={$row['indrelid']} AND attnum $colNumbersSql ORDER BY attnum ASC;"; + + $stmt = $this->_conn->executeQuery($columnNameSql); + $indexColumns = $stmt->fetchAll(); + + // required for getting the order of the columns right. + foreach ($colNumbers AS $colNum) { + foreach ($indexColumns as $colRow) { + if ($colNum == $colRow['attnum']) { + $buffer[] = array( + 'key_name' => $row['relname'], + 'column_name' => trim($colRow['attname']), + 'non_unique' => !$row['indisunique'], + 'primary' => $row['indisprimary'] + ); + } + } + } + } + return parent::_getPortableTableIndexesList($buffer); + } + + protected function _getPortableDatabaseDefinition($database) + { + return $database['datname']; + } + + protected function _getPortableSequenceDefinition($sequence) + { + $data = $this->_conn->fetchAll('SELECT min_value, increment_by FROM ' . $sequence['relname']); + return new Sequence($sequence['relname'], $data[0]['increment_by'], $data[0]['min_value']); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + if (strtolower($tableColumn['type']) === 'varchar') { + // get length from varchar definition + $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']); + $tableColumn['length'] = $length; + } + + $matches = array(); + + $autoincrement = false; + if (preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches)) { + $tableColumn['sequence'] = $matches[1]; + $tableColumn['default'] = null; + $autoincrement = true; + } + + if (stripos($tableColumn['default'], 'NULL') === 0) { + $tableColumn['default'] = null; + } + + $length = (isset($tableColumn['length'])) ? $tableColumn['length'] : null; + if ($length == '-1' && isset($tableColumn['atttypmod'])) { + $length = $tableColumn['atttypmod'] - 4; + } + if ((int) $length <= 0) { + $length = null; + } + $type = array(); + $fixed = null; + + if (!isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + + if ($this->_platform->hasDoctrineTypeMappingFor($tableColumn['type'])) { + $dbType = strtolower($tableColumn['type']); + } else { + $dbType = strtolower($tableColumn['domain_type']); + $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + } + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + switch ($dbType) { + case 'smallint': + case 'int2': + $length = null; + break; + case 'int': + case 'int4': + case 'integer': + $length = null; + break; + case 'bigint': + case 'int8': + $length = null; + break; + case 'bool': + case 'boolean': + $length = null; + break; + case 'text': + $fixed = false; + break; + case 'varchar': + case 'interval': + case '_varchar': + $fixed = false; + break; + case 'char': + case 'bpchar': + $fixed = true; + break; + case 'float': + case 'float4': + case 'float8': + case 'double': + case 'double precision': + case 'real': + case 'decimal': + case 'money': + case 'numeric': + if (preg_match('([A-Za-z]+\(([0-9]+)\,([0-9]+)\))', $tableColumn['complete_type'], $match)) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + break; + case 'year': + $length = null; + break; + } + + $options = array( + 'length' => $length, + 'notnull' => (bool) $tableColumn['isnotnull'], + 'default' => $tableColumn['default'], + 'primary' => (bool) ($tableColumn['pri'] == 't'), + 'precision' => $precision, + 'scale' => $scale, + 'fixed' => $fixed, + 'unsigned' => false, + 'autoincrement' => $autoincrement, + ); + + return new Column($tableColumn['field'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Schema.php b/Doctrine/DBAL/Schema/Schema.php new file mode 100644 index 0000000..89243f8 --- /dev/null +++ b/Doctrine/DBAL/Schema/Schema.php @@ -0,0 +1,327 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\CreateSchemaSqlCollector; +use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector; +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Object representation of a database schema + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Schema extends AbstractAsset +{ + /** + * @var array + */ + protected $_tables = array(); + + /** + * @var array + */ + protected $_sequences = array(); + + /** + * @var SchemaConfig + */ + protected $_schemaConfig = false; + + /** + * @param array $tables + * @param array $sequences + * @param array $views + * @param array $triggers + * @param SchemaConfig $schemaConfig + */ + public function __construct(array $tables=array(), array $sequences=array(), SchemaConfig $schemaConfig=null) + { + if ($schemaConfig == null) { + $schemaConfig = new SchemaConfig(); + } + $this->_schemaConfig = $schemaConfig; + + foreach ($tables AS $table) { + $this->_addTable($table); + } + foreach ($sequences AS $sequence) { + $this->_addSequence($sequence); + } + } + + /** + * @return bool + */ + public function hasExplicitForeignKeyIndexes() + { + return $this->_schemaConfig->hasExplicitForeignKeyIndexes(); + } + + /** + * @param Table $table + */ + protected function _addTable(Table $table) + { + $tableName = strtolower($table->getName()); + if(isset($this->_tables[$tableName])) { + throw SchemaException::tableAlreadyExists($tableName); + } + + $this->_tables[$tableName] = $table; + $table->setSchemaConfig($this->_schemaConfig); + } + + /** + * @param Sequence $sequence + */ + protected function _addSequence(Sequence $sequence) + { + $seqName = strtolower($sequence->getName()); + if (isset($this->_sequences[$seqName])) { + throw SchemaException::sequenceAlreadyExists($seqName); + } + $this->_sequences[$seqName] = $sequence; + } + + /** + * Get all tables of this schema. + * + * @return array + */ + public function getTables() + { + return $this->_tables; + } + + /** + * @param string $tableName + * @return Table + */ + public function getTable($tableName) + { + $tableName = strtolower($tableName); + if (!isset($this->_tables[$tableName])) { + throw SchemaException::tableDoesNotExist($tableName); + } + + return $this->_tables[$tableName]; + } + + /** + * Does this schema have a table with the given name? + * + * @param string $tableName + * @return Schema + */ + public function hasTable($tableName) + { + $tableName = strtolower($tableName); + return isset($this->_tables[$tableName]); + } + + /** + * @param string $sequenceName + * @return bool + */ + public function hasSequence($sequenceName) + { + $sequenceName = strtolower($sequenceName); + return isset($this->_sequences[$sequenceName]); + } + + /** + * @throws SchemaException + * @param string $sequenceName + * @return Doctrine\DBAL\Schema\Sequence + */ + public function getSequence($sequenceName) + { + $sequenceName = strtolower($sequenceName); + if(!$this->hasSequence($sequenceName)) { + throw SchemaException::sequenceDoesNotExist($sequenceName); + } + return $this->_sequences[$sequenceName]; + } + + /** + * @return Doctrine\DBAL\Schema\Sequence[] + */ + public function getSequences() + { + return $this->_sequences; + } + + /** + * Create a new table + * + * @param string $tableName + * @return Table + */ + public function createTable($tableName) + { + $table = new Table($tableName); + $this->_addTable($table); + return $table; + } + + /** + * Rename a table + * + * @param string $oldTableName + * @param string $newTableName + * @return Schema + */ + public function renameTable($oldTableName, $newTableName) + { + $table = $this->getTable($oldTableName); + $table->_setName($newTableName); + + $this->dropTable($oldTableName); + $this->_addTable($table); + return $this; + } + + /** + * Drop a table from the schema. + * + * @param string $tableName + * @return Schema + */ + public function dropTable($tableName) + { + $tableName = strtolower($tableName); + $table = $this->getTable($tableName); + unset($this->_tables[$tableName]); + return $this; + } + + /** + * Create a new sequence + * + * @param string $sequenceName + * @param int $allocationSize + * @param int $initialValue + * @return Sequence + */ + public function createSequence($sequenceName, $allocationSize=1, $initialValue=1) + { + $seq = new Sequence($sequenceName, $allocationSize, $initialValue); + $this->_addSequence($seq); + return $seq; + } + + /** + * @param string $sequenceName + * @return Schema + */ + public function dropSequence($sequenceName) + { + $sequenceName = strtolower($sequenceName); + unset($this->_sequences[$sequenceName]); + return $this; + } + + /** + * Return an array of necessary sql queries to create the schema on the given platform. + * + * @param AbstractPlatform $platform + * @return array + */ + public function toSql(\Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $sqlCollector = new CreateSchemaSqlCollector($platform); + $this->visit($sqlCollector); + + return $sqlCollector->getQueries(); + } + + /** + * Return an array of necessary sql queries to drop the schema on the given platform. + * + * @param AbstractPlatform $platform + * @return array + */ + public function toDropSql(\Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $dropSqlCollector = new DropSchemaSqlCollector($platform); + $this->visit($dropSqlCollector); + + return $dropSqlCollector->getQueries(); + } + + /** + * @param Schema $toSchema + * @param AbstractPlatform $platform + */ + public function getMigrateToSql(Schema $toSchema, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $comparator = new Comparator(); + $schemaDiff = $comparator->compare($this, $toSchema); + return $schemaDiff->toSql($platform); + } + + /** + * @param Schema $fromSchema + * @param AbstractPlatform $platform + */ + public function getMigrateFromSql(Schema $fromSchema, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + $comparator = new Comparator(); + $schemaDiff = $comparator->compare($fromSchema, $this); + return $schemaDiff->toSql($platform); + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptSchema($this); + + foreach ($this->_tables AS $table) { + $table->visit($visitor); + } + foreach ($this->_sequences AS $sequence) { + $sequence->visit($visitor); + } + } + + /** + * Cloning a Schema triggers a deep clone of all related assets. + * + * @return void + */ + public function __clone() + { + foreach ($this->_tables AS $k => $table) { + $this->_tables[$k] = clone $table; + } + foreach ($this->_sequences AS $k => $sequence) { + $this->_sequences[$k] = clone $sequence; + } + } +} diff --git a/Doctrine/DBAL/Schema/SchemaConfig.php b/Doctrine/DBAL/Schema/SchemaConfig.php new file mode 100644 index 0000000..291babb --- /dev/null +++ b/Doctrine/DBAL/Schema/SchemaConfig.php @@ -0,0 +1,76 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Configuration for a Schema + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class SchemaConfig +{ + /** + * @var bool + */ + protected $_hasExplicitForeignKeyIndexes = false; + + /** + * @var int + */ + protected $_maxIdentifierLength = 63; + + /** + * @return bool + */ + public function hasExplicitForeignKeyIndexes() + { + return $this->_hasExplicitForeignKeyIndexes; + } + + /** + * @param bool $flag + */ + public function setExplicitForeignKeyIndexes($flag) + { + $this->_hasExplicitForeignKeyIndexes = (bool)$flag; + } + + /** + * @param int $length + */ + public function setMaxIdentifierLength($length) + { + $this->_maxIdentifierLength = (int)$length; + } + + /** + * @return int + */ + public function getMaxIdentifierLength() + { + return $this->_maxIdentifierLength; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/SchemaDiff.php b/Doctrine/DBAL/Schema/SchemaDiff.php new file mode 100644 index 0000000..8e1fc24 --- /dev/null +++ b/Doctrine/DBAL/Schema/SchemaDiff.php @@ -0,0 +1,177 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use \Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Schema Diff + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class SchemaDiff +{ + /** + * All added tables + * + * @var array(string=>ezcDbSchemaTable) + */ + public $newTables = array(); + + /** + * All changed tables + * + * @var array(string=>ezcDbSchemaTableDiff) + */ + public $changedTables = array(); + + /** + * All removed tables + * + * @var array(string=>Table) + */ + public $removedTables = array(); + + /** + * @var array + */ + public $newSequences = array(); + + /** + * @var array + */ + public $changedSequences = array(); + + /** + * @var array + */ + public $removedSequences = array(); + + /** + * @var array + */ + public $orphanedForeignKeys = array(); + + /** + * Constructs an SchemaDiff object. + * + * @param array(string=>Table) $newTables + * @param array(string=>TableDiff) $changedTables + * @param array(string=>bool) $removedTables + */ + public function __construct($newTables = array(), $changedTables = array(), $removedTables = array()) + { + $this->newTables = $newTables; + $this->changedTables = $changedTables; + $this->removedTables = $removedTables; + } + + /** + * The to save sql mode ensures that the following things don't happen: + * + * 1. Tables are deleted + * 2. Sequences are deleted + * 3. Foreign Keys which reference tables that would otherwise be deleted. + * + * This way it is ensured that assets are deleted which might not be relevant to the metadata schema at all. + * + * @param AbstractPlatform $platform + * @return array + */ + public function toSaveSql(AbstractPlatform $platform) + { + return $this->_toSql($platform, true); + } + + /** + * @param AbstractPlatform $platform + * @return array + */ + public function toSql(AbstractPlatform $platform) + { + return $this->_toSql($platform, false); + } + + /** + * @param AbstractPlatform $platform + * @param bool $saveMode + * @return array + */ + protected function _toSql(AbstractPlatform $platform, $saveMode = false) + { + $sql = array(); + + if ($platform->supportsForeignKeyConstraints() && $saveMode == false) { + foreach ($this->orphanedForeignKeys AS $orphanedForeignKey) { + $sql[] = $platform->getDropForeignKeySQL($orphanedForeignKey, $orphanedForeignKey->getLocalTableName()); + } + } + + if ($platform->supportsSequences() == true) { + foreach ($this->changedSequences AS $sequence) { + $sql[] = $platform->getDropSequenceSQL($sequence); + $sql[] = $platform->getCreateSequenceSQL($sequence); + } + + if ($saveMode === false) { + foreach ($this->removedSequences AS $sequence) { + $sql[] = $platform->getDropSequenceSQL($sequence); + } + } + + foreach ($this->newSequences AS $sequence) { + $sql[] = $platform->getCreateSequenceSQL($sequence); + } + } + + $foreignKeySql = array(); + foreach ($this->newTables AS $table) { + $sql = array_merge( + $sql, + $platform->getCreateTableSQL($table, AbstractPlatform::CREATE_INDEXES) + ); + + if ($platform->supportsForeignKeyConstraints()) { + foreach ($table->getForeignKeys() AS $foreignKey) { + $foreignKeySql[] = $platform->getCreateForeignKeySQL($foreignKey, $table); + } + } + } + $sql = array_merge($sql, $foreignKeySql); + + if ($saveMode === false) { + foreach ($this->removedTables AS $table) { + $sql[] = $platform->getDropTableSQL($table); + } + } + + foreach ($this->changedTables AS $tableDiff) { + $sql = array_merge($sql, $platform->getAlterTableSQL($tableDiff)); + } + + return $sql; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/SchemaException.php b/Doctrine/DBAL/Schema/SchemaException.php new file mode 100644 index 0000000..a8cb93d --- /dev/null +++ b/Doctrine/DBAL/Schema/SchemaException.php @@ -0,0 +1,126 @@ +getName()." requires a named foreign key, ". + "but the given foreign key from (".implode(", ", $foreignKey->getColumns()).") onto foreign table ". + "'".$foreignKey->getForeignTableName()."' (".implode(", ", $foreignKey->getForeignColumns()).") is currently ". + "unnamed." + ); + } + + static public function alterTableChangeNotSupported($changeName) { + return new self ("Alter table change not supported, given '$changeName'"); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Sequence.php b/Doctrine/DBAL/Schema/Sequence.php new file mode 100644 index 0000000..369f0e8 --- /dev/null +++ b/Doctrine/DBAL/Schema/Sequence.php @@ -0,0 +1,77 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Schema\Visitor\Visitor; + +/** + * Sequence Structure + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Sequence extends AbstractAsset +{ + /** + * @var int + */ + protected $_allocationSize = 1; + + /** + * @var int + */ + protected $_initialValue = 1; + + /** + * + * @param string $name + * @param int $allocationSize + * @param int $initialValue + */ + public function __construct($name, $allocationSize=1, $initialValue=1) + { + $this->_setName($name); + $this->_allocationSize = (is_numeric($allocationSize))?$allocationSize:1; + $this->_initialValue = (is_numeric($initialValue))?$initialValue:1; + } + + public function getAllocationSize() + { + return $this->_allocationSize; + } + + public function getInitialValue() + { + return $this->_initialValue; + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptSequence($this); + } +} diff --git a/Doctrine/DBAL/Schema/SqliteSchemaManager.php b/Doctrine/DBAL/Schema/SqliteSchemaManager.php new file mode 100644 index 0000000..5c7d005 --- /dev/null +++ b/Doctrine/DBAL/Schema/SqliteSchemaManager.php @@ -0,0 +1,181 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * SqliteSchemaManager + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Konsta Vesterinen + * @author Lukas Smith (PEAR MDB2 library) + * @author Jonathan H. Wage + * @version $Revision$ + * @since 2.0 + */ +class SqliteSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritdoc} + * + * @override + */ + public function dropDatabase($database) + { + if (file_exists($database)) { + unlink($database); + } + } + + /** + * {@inheritdoc} + * + * @override + */ + public function createDatabase($database) + { + $params = $this->_conn->getParams(); + $driver = $params['driver']; + $options = array( + 'driver' => $driver, + 'path' => $database + ); + $conn = \Doctrine\DBAL\DriverManager::getConnection($options); + $conn->connect(); + $conn->close(); + } + + protected function _getPortableTableDefinition($table) + { + return $table['name']; + } + + /** + * @license New BSD License + * @link http://ezcomponents.org/docs/api/trunk/DatabaseSchema/ezcDbSchemaPgsqlReader.html + * @param array $tableIndexes + * @param string $tableName + * @return array + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName=null) + { + $indexBuffer = array(); + + // fetch primary + $stmt = $this->_conn->executeQuery( "PRAGMA TABLE_INFO ('$tableName')" ); + $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC); + foreach($indexArray AS $indexColumnRow) { + if($indexColumnRow['pk'] == "1") { + $indexBuffer[] = array( + 'key_name' => 'primary', + 'primary' => true, + 'non_unique' => false, + 'column_name' => $indexColumnRow['name'] + ); + } + } + + // fetch regular indexes + foreach($tableIndexes AS $tableIndex) { + $keyName = $tableIndex['name']; + $idx = array(); + $idx['key_name'] = $keyName; + $idx['primary'] = false; + $idx['non_unique'] = $tableIndex['unique']?false:true; + + $stmt = $this->_conn->executeQuery( "PRAGMA INDEX_INFO ( '{$keyName}' )" ); + $indexArray = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + foreach ( $indexArray as $indexColumnRow ) { + $idx['column_name'] = $indexColumnRow['name']; + $indexBuffer[] = $idx; + } + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + protected function _getPortableTableIndexDefinition($tableIndex) + { + return array( + 'name' => $tableIndex['name'], + 'unique' => (bool) $tableIndex['unique'] + ); + } + + protected function _getPortableTableColumnDefinition($tableColumn) + { + $e = explode('(', $tableColumn['type']); + $tableColumn['type'] = $e[0]; + if (isset($e[1])) { + $length = trim($e[1], ')'); + $tableColumn['length'] = $length; + } + + $dbType = strtolower($tableColumn['type']); + $length = isset($tableColumn['length']) ? $tableColumn['length'] : null; + $unsigned = (boolean) isset($tableColumn['unsigned']) ? $tableColumn['unsigned'] : false; + $fixed = false; + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $default = $tableColumn['dflt_value']; + if ($default == 'NULL') { + $default = null; + } + $notnull = (bool) $tableColumn['notnull']; + + if ( ! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + + switch ($dbType) { + case 'char': + $fixed = true; + break; + case 'float': + case 'double': + case 'real': + case 'decimal': + case 'numeric': + list($precision, $scale) = array_map('trim', explode(', ', $tableColumn['length'])); + $length = null; + break; + } + + $options = array( + 'length' => $length, + 'unsigned' => (bool) $unsigned, + 'fixed' => $fixed, + 'notnull' => $notnull, + 'default' => $default, + 'precision' => $precision, + 'scale' => $scale, + 'autoincrement' => (bool) $tableColumn['pk'], + ); + + return new Column($tableColumn['name'], \Doctrine\DBAL\Types\Type::getType($type), $options); + } + + protected function _getPortableViewDefinition($view) + { + return new View($view['name'], $view['sql']); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Table.php b/Doctrine/DBAL/Schema/Table.php new file mode 100644 index 0000000..3b78596 --- /dev/null +++ b/Doctrine/DBAL/Schema/Table.php @@ -0,0 +1,619 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Schema\Visitor\Visitor; +use Doctrine\DBAL\DBALException; + +/** + * Object Representation of a table + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class Table extends AbstractAsset +{ + /** + * @var string + */ + protected $_name = null; + + /** + * @var array + */ + protected $_columns = array(); + + /** + * @var array + */ + protected $_indexes = array(); + + /** + * @var string + */ + protected $_primaryKeyName = false; + + /** + * @var array + */ + protected $_fkConstraints = array(); + + /** + * @var array + */ + protected $_options = array(); + + /** + * @var SchemaConfig + */ + protected $_schemaConfig = null; + + /** + * + * @param string $tableName + * @param array $columns + * @param array $indexes + * @param array $fkConstraints + * @param int $idGeneratorType + * @param array $options + */ + public function __construct($tableName, array $columns=array(), array $indexes=array(), array $fkConstraints=array(), $idGeneratorType = 0, array $options=array()) + { + if (strlen($tableName) == 0) { + throw DBALException::invalidTableName($tableName); + } + + $this->_setName($tableName); + $this->_idGeneratorType = $idGeneratorType; + + foreach ($columns AS $column) { + $this->_addColumn($column); + } + + foreach ($indexes AS $idx) { + $this->_addIndex($idx); + } + + foreach ($fkConstraints AS $constraint) { + $this->_addForeignKeyConstraint($constraint); + } + + $this->_options = $options; + } + + /** + * @param SchemaConfig $schemaConfig + */ + public function setSchemaConfig(SchemaConfig $schemaConfig) + { + $this->_schemaConfig = $schemaConfig; + } + + /** + * @return int + */ + protected function _getMaxIdentifierLength() + { + if ($this->_schemaConfig instanceof SchemaConfig) { + return $this->_schemaConfig->getMaxIdentifierLength(); + } else { + return 63; + } + } + + /** + * Set Primary Key + * + * @param array $columns + * @param string $indexName + * @return Table + */ + public function setPrimaryKey(array $columns, $indexName = false) + { + $primaryKey = $this->_createIndex($columns, $indexName ?: "primary", true, true); + + foreach ($columns AS $columnName) { + $column = $this->getColumn($columnName); + $column->setNotnull(true); + } + + return $primaryKey; + } + + /** + * @param array $columnNames + * @param string $indexName + * @return Table + */ + public function addIndex(array $columnNames, $indexName = null) + { + if($indexName == null) { + $indexName = $this->_generateIdentifierName( + array_merge(array($this->getName()), $columnNames), "idx", $this->_getMaxIdentifierLength() + ); + } + + return $this->_createIndex($columnNames, $indexName, false, false); + } + + /** + * + * @param array $columnNames + * @param string $indexName + * @return Table + */ + public function addUniqueIndex(array $columnNames, $indexName = null) + { + if ($indexName == null) { + $indexName = $this->_generateIdentifierName( + array_merge(array($this->getName()), $columnNames), "uniq", $this->_getMaxIdentifierLength() + ); + } + + return $this->_createIndex($columnNames, $indexName, true, false); + } + + /** + * Check if an index begins in the order of the given columns. + * + * @param array $columnsNames + * @return bool + */ + public function columnsAreIndexed(array $columnsNames) + { + foreach ($this->getIndexes() AS $index) { + /* @var $index Index */ + if ($index->spansColumns($columnsNames)) { + return true; + } + } + return false; + } + + /** + * + * @param array $columnNames + * @param string $indexName + * @param bool $isUnique + * @param bool $isPrimary + * @return Table + */ + private function _createIndex(array $columnNames, $indexName, $isUnique, $isPrimary) + { + if (preg_match('(([^a-zA-Z0-9_]+))', $indexName)) { + throw SchemaException::indexNameInvalid($indexName); + } + + foreach ($columnNames AS $columnName => $indexColOptions) { + if (is_numeric($columnName) && is_string($indexColOptions)) { + $columnName = $indexColOptions; + } + + if ( ! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + $this->_addIndex(new Index($indexName, $columnNames, $isUnique, $isPrimary)); + return $this; + } + + /** + * @param string $columnName + * @param string $columnType + * @param array $options + * @return Column + */ + public function addColumn($columnName, $typeName, array $options=array()) + { + $column = new Column($columnName, Type::getType($typeName), $options); + + $this->_addColumn($column); + return $column; + } + + /** + * Rename Column + * + * @param string $oldColumnName + * @param string $newColumnName + * @return Table + */ + public function renameColumn($oldColumnName, $newColumnName) + { + $column = $this->getColumn($oldColumnName); + $this->dropColumn($oldColumnName); + + $column->_setName($newColumnName); + return $this; + } + + /** + * Change Column Details + * + * @param string $columnName + * @param array $options + * @return Table + */ + public function changeColumn($columnName, array $options) + { + $column = $this->getColumn($columnName); + $column->setOptions($options); + return $this; + } + + /** + * Drop Column from Table + * + * @param string $columnName + * @return Table + */ + public function dropColumn($columnName) + { + $columnName = strtolower($columnName); + $column = $this->getColumn($columnName); + unset($this->_columns[$columnName]); + return $this; + } + + + /** + * Add a foreign key constraint + * + * Name is inferred from the local columns + * + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @return Table + */ + public function addForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array()) + { + $name = $this->_generateIdentifierName(array_merge((array)$this->getName(), $localColumnNames), "fk", $this->_getMaxIdentifierLength()); + return $this->addNamedForeignKeyConstraint($name, $foreignTable, $localColumnNames, $foreignColumnNames, $options); + } + + /** + * Add a foreign key constraint + * + * Name is to be generated by the database itsself. + * + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @return Table + */ + public function addUnnamedForeignKeyConstraint($foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array()) + { + return $this->addNamedForeignKeyConstraint(null, $foreignTable, $localColumnNames, $foreignColumnNames, $options); + } + + /** + * Add a foreign key constraint with a given name + * + * @param string $name + * @param Table $foreignTable + * @param array $localColumns + * @param array $foreignColumns + * @param array $options + * @return Table + */ + public function addNamedForeignKeyConstraint($name, $foreignTable, array $localColumnNames, array $foreignColumnNames, array $options=array()) + { + if ($foreignTable instanceof Table) { + $foreignTableName = $foreignTable->getName(); + + foreach ($foreignColumnNames AS $columnName) { + if ( ! $foreignTable->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName()); + } + } + } else { + $foreignTableName = $foreignTable; + } + + foreach ($localColumnNames AS $columnName) { + if ( ! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + + $constraint = new ForeignKeyConstraint( + $localColumnNames, $foreignTableName, $foreignColumnNames, $name, $options + ); + $this->_addForeignKeyConstraint($constraint); + + return $this; + } + + /** + * @param string $name + * @param string $value + * @return Table + */ + public function addOption($name, $value) + { + $this->_options[$name] = $value; + return $this; + } + + /** + * @param Column $column + */ + protected function _addColumn(Column $column) + { + $columnName = $column->getName(); + $columnName = strtolower($columnName); + + if (isset($this->_columns[$columnName])) { + throw SchemaException::columnAlreadyExists($this->getName(), $columnName); + } + + $this->_columns[$columnName] = $column; + } + + /** + * Add index to table + * + * @param Index $indexCandidate + * @return Table + */ + protected function _addIndex(Index $indexCandidate) + { + // check for duplicates + foreach ($this->_indexes AS $existingIndex) { + if ($indexCandidate->isFullfilledBy($existingIndex)) { + return $this; + } + } + + $indexName = $indexCandidate->getName(); + $indexName = strtolower($indexName); + + if (isset($this->_indexes[$indexName]) || ($this->_primaryKeyName != false && $indexCandidate->isPrimary())) { + throw SchemaException::indexAlreadyExists($indexName, $this->_name); + } + + // remove overruled indexes + foreach ($this->_indexes AS $idxKey => $existingIndex) { + if ($indexCandidate->overrules($existingIndex)) { + unset($this->_indexes[$idxKey]); + } + } + + if ($indexCandidate->isPrimary()) { + $this->_primaryKeyName = $indexName; + } + + $this->_indexes[$indexName] = $indexCandidate; + return $this; + } + + /** + * @param ForeignKeyConstraint $constraint + */ + protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) + { + $constraint->setLocalTable($this); + + if(strlen($constraint->getName())) { + $name = $constraint->getName(); + } else { + $name = $this->_generateIdentifierName( + array_merge((array)$this->getName(), $constraint->getLocalColumns()), "fk", $this->_getMaxIdentifierLength() + ); + } + $name = strtolower($name); + + $this->_fkConstraints[$name] = $constraint; + // add an explicit index on the foreign key columns. If there is already an index that fullfils this requirements drop the request. + // In the case of __construct calling this method during hydration from schema-details all the explicitly added indexes + // lead to duplicates. This creates compuation overhead in this case, however no duplicate indexes are ever added (based on columns). + $this->addIndex($constraint->getColumns()); + } + + /** + * Does Table have a foreign key constraint with the given name? + * * + * @param string $constraintName + * @return bool + */ + public function hasForeignKey($constraintName) + { + $constraintName = strtolower($constraintName); + return isset($this->_fkConstraints[$constraintName]); + } + + /** + * @param string $constraintName + * @return ForeignKeyConstraint + */ + public function getForeignKey($constraintName) + { + $constraintName = strtolower($constraintName); + if(!$this->hasForeignKey($constraintName)) { + throw SchemaException::foreignKeyDoesNotExist($constraintName, $this->_name); + } + + return $this->_fkConstraints[$constraintName]; + } + + /** + * @return Column[] + */ + public function getColumns() + { + $columns = $this->_columns; + + $pkCols = array(); + $fkCols = array(); + + if ($this->hasIndex($this->_primaryKeyName)) { + $pkCols = $this->getPrimaryKey()->getColumns(); + } + foreach ($this->getForeignKeys() AS $fk) { + /* @var $fk ForeignKeyConstraint */ + $fkCols = array_merge($fkCols, $fk->getColumns()); + } + $colNames = array_unique(array_merge($pkCols, $fkCols, array_keys($columns))); + + uksort($columns, function($a, $b) use($colNames) { + return (array_search($a, $colNames) >= array_search($b, $colNames)); + }); + return $columns; + } + + + /** + * Does this table have a column with the given name? + * + * @param string $columnName + * @return bool + */ + public function hasColumn($columnName) + { + $columnName = strtolower($columnName); + return isset($this->_columns[$columnName]); + } + + /** + * Get a column instance + * + * @param string $columnName + * @return Column + */ + public function getColumn($columnName) + { + $columnName = strtolower($columnName); + if (!$this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + + return $this->_columns[$columnName]; + } + + /** + * @return Index + */ + public function getPrimaryKey() + { + return $this->getIndex($this->_primaryKeyName); + } + + /** + * @param string $indexName + * @return bool + */ + public function hasIndex($indexName) + { + $indexName = strtolower($indexName); + return (isset($this->_indexes[$indexName])); + } + + /** + * @param string $indexName + * @return Index + */ + public function getIndex($indexName) + { + $indexName = strtolower($indexName); + if (!$this->hasIndex($indexName)) { + throw SchemaException::indexDoesNotExist($indexName, $this->_name); + } + return $this->_indexes[$indexName]; + } + + /** + * @return array + */ + public function getIndexes() + { + return $this->_indexes; + } + + /** + * Get Constraints + * + * @return array + */ + public function getForeignKeys() + { + return $this->_fkConstraints; + } + + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + public function getOption($name) + { + return $this->_options[$name]; + } + + public function getOptions() + { + return $this->_options; + } + + /** + * @param Visitor $visitor + */ + public function visit(Visitor $visitor) + { + $visitor->acceptTable($this); + + foreach ($this->getColumns() AS $column) { + $visitor->acceptColumn($this, $column); + } + + foreach ($this->getIndexes() AS $index) { + $visitor->acceptIndex($this, $index); + } + + foreach ($this->getForeignKeys() AS $constraint) { + $visitor->acceptForeignKey($this, $constraint); + } + } + + /** + * Clone of a Table triggers a deep clone of all affected assets + */ + public function __clone() + { + foreach ($this->_columns AS $k => $column) { + $this->_columns[$k] = clone $column; + } + foreach ($this->_indexes AS $k => $index) { + $this->_indexes[$k] = clone $index; + } + foreach ($this->_fkConstraints AS $k => $fk) { + $this->_fkConstraints[$k] = clone $fk; + $this->_fkConstraints[$k]->setLocalTable($this); + } + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/TableDiff.php b/Doctrine/DBAL/Schema/TableDiff.php new file mode 100644 index 0000000..3351a23 --- /dev/null +++ b/Doctrine/DBAL/Schema/TableDiff.php @@ -0,0 +1,139 @@ +. + */ + +namespace Doctrine\DBAL\Schema; + +/** + * Table Diff + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @copyright Copyright (C) 2005-2009 eZ Systems AS. All rights reserved. + * @license http://ez.no/licenses/new_bsd New BSD License + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class TableDiff +{ + /** + * @var string + */ + public $name = null; + + /** + * @var string + */ + public $newName = false; + + /** + * All added fields + * + * @var array(string=>Column) + */ + public $addedColumns; + + /** + * All changed fields + * + * @var array(string=>Column) + */ + public $changedColumns = array(); + + /** + * All removed fields + * + * @var array(string=>bool) + */ + public $removedColumns = array(); + + /** + * Columns that are only renamed from key to column instance name. + * + * @var array(string=>Column) + */ + public $renamedColumns = array(); + + /** + * All added indexes + * + * @var array(string=>Index) + */ + public $addedIndexes = array(); + + /** + * All changed indexes + * + * @var array(string=>Index) + */ + public $changedIndexes = array(); + + /** + * All removed indexes + * + * @var array(string=>bool) + */ + public $removedIndexes = array(); + + /** + * All added foreign key definitions + * + * @var array + */ + public $addedForeignKeys = array(); + + /** + * All changed foreign keys + * + * @var array + */ + public $changedForeignKeys = array(); + + /** + * All removed foreign keys + * + * @var array + */ + public $removedForeignKeys = array(); + + /** + * Constructs an TableDiff object. + * + * @param array(string=>Column) $addedColumns + * @param array(string=>Column) $changedColumns + * @param array(string=>bool) $removedColumns + * @param array(string=>Index) $addedIndexes + * @param array(string=>Index) $changedIndexes + * @param array(string=>bool) $removedIndexes + */ + public function __construct($tableName, $addedColumns = array(), + $changedColumns = array(), $removedColumns = array(), $addedIndexes = array(), + $changedIndexes = array(), $removedIndexes = array()) + { + $this->name = $tableName; + $this->addedColumns = $addedColumns; + $this->changedColumns = $changedColumns; + $this->removedColumns = $removedColumns; + $this->addedIndexes = $addedIndexes; + $this->changedIndexes = $changedIndexes; + $this->removedIndexes = $removedIndexes; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/View.php b/Doctrine/DBAL/Schema/View.php new file mode 100644 index 0000000..4a8329d --- /dev/null +++ b/Doctrine/DBAL/Schema/View.php @@ -0,0 +1,53 @@ +. +*/ + +namespace Doctrine\DBAL\Schema; + +/** + * Representation of a Database View + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class View extends AbstractAsset +{ + /** + * @var string + */ + private $_sql; + + public function __construct($name, $sql) + { + $this->_setName($name); + $this->_sql = $sql; + } + + /** + * @return string + */ + public function getSql() + { + return $this->_sql; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php b/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php new file mode 100644 index 0000000..212fa2b --- /dev/null +++ b/Doctrine/DBAL/Schema/Visitor/CreateSchemaSqlCollector.php @@ -0,0 +1,147 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +class CreateSchemaSqlCollector implements Visitor +{ + /** + * @var array + */ + private $_createTableQueries = array(); + + /** + * @var array + */ + private $_createSequenceQueries = array(); + + /** + * @var array + */ + private $_createFkConstraintQueries = array(); + + /** + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @param AbstractPlatform $platform + */ + public function __construct(AbstractPlatform $platform) + { + $this->_platform = $platform; + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + + } + + /** + * Generate DDL Statements to create the accepted table with all its dependencies. + * + * @param Table $table + */ + public function acceptTable(Table $table) + { + $this->_createTableQueries = array_merge($this->_createTableQueries, + $this->_platform->getCreateTableSQL($table) + ); + } + + public function acceptColumn(Table $table, Column $column) + { + + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + // Append the foreign key constraints SQL + if ($this->_platform->supportsForeignKeyConstraints()) { + $this->_createFkConstraintQueries = array_merge($this->_createFkConstraintQueries, + (array) $this->_platform->getCreateForeignKeySQL( + $fkConstraint, $localTable->getQuotedName($this->_platform) + ) + ); + } + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + $this->_createSequenceQueries = array_merge( + $this->_createSequenceQueries, (array)$this->_platform->getCreateSequenceSQL($sequence) + ); + } + + /** + * @return array + */ + public function resetQueries() + { + $this->_createTableQueries = array(); + $this->_createSequenceQueries = array(); + $this->_createFkConstraintQueries = array(); + } + + /** + * Get all queries collected so far. + * + * @return array + */ + public function getQueries() + { + return array_merge( + $this->_createTableQueries, + $this->_createSequenceQueries, + $this->_createFkConstraintQueries + ); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php b/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php new file mode 100644 index 0000000..7672364 --- /dev/null +++ b/Doctrine/DBAL/Schema/Visitor/DropSchemaSqlCollector.php @@ -0,0 +1,142 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +/** + * Gather SQL statements that allow to completly drop the current schema. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class DropSchemaSqlCollector implements Visitor +{ + /** + * @var array + */ + private $_constraints = array(); + + /** + * @var array + */ + private $_sequences = array(); + + /** + * @var array + */ + private $_tables = array(); + + /** + * + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform = null; + + /** + * @param AbstractPlatform $platform + */ + public function __construct(AbstractPlatform $platform) + { + $this->_platform = $platform; + } + + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema) + { + + } + + /** + * @param Table $table + */ + public function acceptTable(Table $table) + { + $this->_tables[] = $this->_platform->getDropTableSQL($table->getQuotedName($this->_platform)); + } + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column) + { + + } + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + if (strlen($fkConstraint->getName()) == 0) { + throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint); + } + + $this->_constraints[] = $this->_platform->getDropForeignKeySQL($fkConstraint->getQuotedName($this->_platform), $localTable->getQuotedName($this->_platform)); + } + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index) + { + + } + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence) + { + $this->_sequences[] = $this->_platform->getDropSequenceSQL($sequence->getQuotedName($this->_platform)); + } + + /** + * @return array + */ + public function clearQueries() + { + $this->_constraints = $this->_sequences = $this->_tables = array(); + } + + /** + * @return array + */ + public function getQueries() + { + return array_merge($this->_constraints, $this->_tables, $this->_sequences); + } +} diff --git a/Doctrine/DBAL/Schema/Visitor/Visitor.php b/Doctrine/DBAL/Schema/Visitor/Visitor.php new file mode 100644 index 0000000..2dad80d --- /dev/null +++ b/Doctrine/DBAL/Schema/Visitor/Visitor.php @@ -0,0 +1,75 @@ +. + */ + +namespace Doctrine\DBAL\Schema\Visitor; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\Schema\Table, + Doctrine\DBAL\Schema\Schema, + Doctrine\DBAL\Schema\Column, + Doctrine\DBAL\Schema\ForeignKeyConstraint, + Doctrine\DBAL\Schema\Constraint, + Doctrine\DBAL\Schema\Sequence, + Doctrine\DBAL\Schema\Index; + +/** + * Schema Visitor used for Validation or Generation purposes. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +interface Visitor +{ + /** + * @param Schema $schema + */ + public function acceptSchema(Schema $schema); + + /** + * @param Table $table + */ + public function acceptTable(Table $table); + + /** + * @param Column $column + */ + public function acceptColumn(Table $table, Column $column); + + /** + * @param Table $localTable + * @param ForeignKeyConstraint $fkConstraint + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint); + + /** + * @param Table $table + * @param Index $index + */ + public function acceptIndex(Table $table, Index $index); + + /** + * @param Sequence $sequence + */ + public function acceptSequence(Sequence $sequence); +} \ No newline at end of file diff --git a/Doctrine/DBAL/Statement.php b/Doctrine/DBAL/Statement.php new file mode 100644 index 0000000..45f1243 --- /dev/null +++ b/Doctrine/DBAL/Statement.php @@ -0,0 +1,237 @@ +. + */ + +namespace Doctrine\DBAL; + +use PDO, + Doctrine\DBAL\Types\Type, + Doctrine\DBAL\Driver\Statement as DriverStatement; + +/** + * A thin wrapper around a Doctrine\DBAL\Driver\Statement that adds support + * for logging, DBAL mapping types, etc. + * + * @author Roman Borschel + * @since 2.0 + */ +class Statement implements DriverStatement +{ + /** + * @var string The SQL statement. + */ + private $_sql; + /** + * @var array The bound parameters. + */ + private $_params = array(); + /** + * @var Doctrine\DBAL\Driver\Statement The underlying driver statement. + */ + private $_stmt; + /** + * @var Doctrine\DBAL\Platforms\AbstractPlatform The underlying database platform. + */ + private $_platform; + /** + * @var Doctrine\DBAL\Connection The connection this statement is bound to and executed on. + */ + private $_conn; + + /** + * Creates a new Statement for the given SQL and Connection. + * + * @param string $sql The SQL of the statement. + * @param Doctrine\DBAL\Connection The connection on which the statement should be executed. + */ + public function __construct($sql, Connection $conn) + { + $this->_sql = $sql; + $this->_stmt = $conn->getWrappedConnection()->prepare($sql); + $this->_conn = $conn; + $this->_platform = $conn->getDatabasePlatform(); + } + + /** + * Binds a parameter value to the statement. + * + * The value can optionally be bound with a PDO binding type or a DBAL mapping type. + * If bound with a DBAL mapping type, the binding type is derived from the mapping + * type and the value undergoes the conversion routines of the mapping type before + * being bound. + * + * @param $name The name or position of the parameter. + * @param $value The value of the parameter. + * @param mixed $type Either a PDO binding type or a DBAL mapping type name or instance. + * @return boolean TRUE on success, FALSE on failure. + */ + public function bindValue($name, $value, $type = null) + { + $this->_params[$name] = $value; + if ($type !== null) { + if (is_string($type)) { + $type = Type::getType($type); + } + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->_platform); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type; // PDO::PARAM_* constants + } + return $this->_stmt->bindValue($name, $value, $bindingType); + } else { + return $this->_stmt->bindValue($name, $value); + } + } + + /** + * Binds a parameter to a value by reference. + * + * Binding a parameter by reference does not support DBAL mapping types. + * + * @param string $name The name or position of the parameter. + * @param mixed $value The reference to the variable to bind + * @param integer $type The PDO binding type. + * @return boolean TRUE on success, FALSE on failure. + */ + public function bindParam($name, &$var, $type = PDO::PARAM_STR) + { + return $this->_stmt->bindParam($name, $var, $type); + } + + /** + * Executes the statement with the currently bound parameters. + * + * @return boolean TRUE on success, FALSE on failure. + */ + public function execute($params = null) + { + $hasLogger = $this->_conn->getConfiguration()->getSQLLogger(); + if ($hasLogger) { + $this->_conn->getConfiguration()->getSQLLogger()->startQuery($this->_sql, $this->_params); + } + + $stmt = $this->_stmt->execute($params); + + if ($hasLogger) { + $this->_conn->getConfiguration()->getSQLLogger()->stopQuery(); + } + $this->_params = array(); + return $stmt; + } + + /** + * Closes the cursor, freeing the database resources used by this statement. + * + * @return boolean TRUE on success, FALSE on failure. + */ + public function closeCursor() + { + return $this->_stmt->closeCursor(); + } + + /** + * Returns the number of columns in the result set. + * + * @return integer + */ + public function columnCount() + { + return $this->_stmt->columnCount(); + } + + /** + * Fetches the SQLSTATE associated with the last operation on the statement. + * + * @return string + */ + public function errorCode() + { + return $this->_stmt->errorCode(); + } + + /** + * Fetches extended error information associated with the last operation on the statement. + * + * @return array + */ + public function errorInfo() + { + return $this->_stmt->errorInfo(); + } + + /** + * Fetches the next row from a result set. + * + * @param integer $fetchStyle + * @return mixed The return value of this function on success depends on the fetch type. + * In all cases, FALSE is returned on failure. + */ + public function fetch($fetchStyle = PDO::FETCH_BOTH) + { + return $this->_stmt->fetch($fetchStyle); + } + + /** + * Returns an array containing all of the result set rows. + * + * @param integer $fetchStyle + * @param integer $columnIndex + * @return array An array containing all of the remaining rows in the result set. + */ + public function fetchAll($fetchStyle = PDO::FETCH_BOTH, $columnIndex = 0) + { + if ($columnIndex != 0) { + return $this->_stmt->fetchAll($fetchStyle, $columnIndex); + } + return $this->_stmt->fetchAll($fetchStyle); + } + + /** + * Returns a single column from the next row of a result set. + * + * @param integer $columnIndex + * @return mixed A single column from the next row of a result set or FALSE if there are no more rows. + */ + public function fetchColumn($columnIndex = 0) + { + return $this->_stmt->fetchColumn($columnIndex); + } + + /** + * Returns the number of rows affected by the last execution of this statement. + * + * @return integer The number of affected rows. + */ + public function rowCount() + { + return $this->_stmt->rowCount(); + } + + /** + * Gets the wrapped driver statement. + * + * @return Doctrine\DBAL\Driver\Statement + */ + public function getWrappedStatement() + { + return $this->_stmt; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php b/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php new file mode 100644 index 0000000..9ae945b --- /dev/null +++ b/Doctrine/DBAL/Tools/Console/Command/ImportCommand.php @@ -0,0 +1,127 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console; + +/** + * Task for executing arbitrary SQL that can come from a file or directly from + * the command line. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ImportCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('dbal:import') + ->setDescription('Import SQL file(s) directly to Database.') + ->setDefinition(array( + new InputArgument( + 'file', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'File path(s) of SQL to be executed.' + ) + )) + ->setHelp(<<getHelper('db')->getConnection(); + + if (($fileNames = $input->getArgument('file')) !== null) { + foreach ((array) $fileNames as $fileName) { + $fileName = realpath($fileName); + + if ( ! file_exists($fileName)) { + throw new \InvalidArgumentException( + sprintf("SQL file '%s' does not exist.", $fileName) + ); + } else if ( ! is_readable($fileName)) { + throw new \InvalidArgumentException( + sprintf("SQL file '%s' does not have read permissions.", $fileName) + ); + } + + $output->write(sprintf("Processing file '%s'... ", $fileName)); + $sql = file_get_contents($fileName); + + if ($conn instanceof \Doctrine\DBAL\Driver\PDOConnection) { + // PDO Drivers + try { + $lines = 0; + + $stmt = $conn->prepare($sql); + $stmt->execute(); + + do { + // Required due to "MySQL has gone away!" issue + $stmt->fetch(); + $stmt->closeCursor(); + + $lines++; + } while ($stmt->nextRowset()); + + $output->write(sprintf('%d statements executed!', $lines) . PHP_EOL); + } catch (\PDOException $e) { + $output->write('error!' . PHP_EOL); + + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); + } + } else { + // Non-PDO Drivers (ie. OCI8 driver) + $stmt = $conn->prepare($sql); + $rs = $stmt->execute(); + + if ($rs) { + $printer->writeln('OK!'); + } else { + $error = $stmt->errorInfo(); + + $output->write('error!' . PHP_EOL); + + throw new \RuntimeException($error[2], $error[0]); + } + + $stmt->closeCursor(); + } + } + } + } +} diff --git a/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php b/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php new file mode 100644 index 0000000..abcbfe4 --- /dev/null +++ b/Doctrine/DBAL/Tools/Console/Command/RunSqlCommand.php @@ -0,0 +1,86 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Task for executing arbitrary SQL that can come from a file or directly from + * the command line. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class RunSqlCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('dbal:run-sql') + ->setDescription('Executes arbitrary SQL directly from the command line.') + ->setDefinition(array( + new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'), + new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set.', 7) + )) + ->setHelp(<<getHelper('db')->getConnection(); + + if (($sql = $input->getArgument('sql')) === null) { + throw new \RuntimeException("Argument 'SQL' is required in order to execute this command correctly."); + } + + $depth = $input->getOption('depth'); + + if ( ! is_numeric($depth)) { + throw new \LogicException("Option 'depth' must contains an integer value"); + } + + if (preg_match('/^select/i', $sql)) { + $resultSet = $conn->fetchAll($sql); + } else { + $resultSet = $conn->executeUpdate($sql); + } + + \Doctrine\Common\Util\Debug::dump($resultSet, (int) $depth); + } +} diff --git a/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php b/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php new file mode 100644 index 0000000..4437d46 --- /dev/null +++ b/Doctrine/DBAL/Tools/Console/Helper/ConnectionHelper.php @@ -0,0 +1,74 @@ +. + */ + +namespace Doctrine\DBAL\Tools\Console\Helper; + +use Symfony\Component\Console\Helper\Helper, + Doctrine\DBAL\Connection; + +/** + * Doctrine CLI Connection Helper. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConnectionHelper extends Helper +{ + /** + * Doctrine Database Connection + * @var Connection + */ + protected $_connection; + + /** + * Constructor + * + * @param Connection $connection Doctrine Database Connection + */ + public function __construct(Connection $connection) + { + $this->_connection = $connection; + } + + /** + * Retrieves Doctrine Database Connection + * + * @return Connection + */ + public function getConnection() + { + return $this->_connection; + } + + /** + * @see Helper + */ + public function getName() + { + return 'connection'; + } +} diff --git a/Doctrine/DBAL/Types/ArrayType.php b/Doctrine/DBAL/Types/ArrayType.php new file mode 100644 index 0000000..2357207 --- /dev/null +++ b/Doctrine/DBAL/Types/ArrayType.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +/** + * Type that maps a PHP array to a clob SQL type. + * + * @since 2.0 + */ +class ArrayType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return serialize($value); + } + + public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + $val = unserialize($value); + if ($val === false) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } + + public function getName() + { + return Type::TARRAY; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/BigIntType.php b/Doctrine/DBAL/Types/BigIntType.php new file mode 100644 index 0000000..3ab551d --- /dev/null +++ b/Doctrine/DBAL/Types/BigIntType.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a database BIGINT to a PHP string. + * + * @author robo + * @since 2.0 + */ +class BigIntType extends Type +{ + public function getName() + { + return Type::BIGINT; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration); + } + + public function getBindingType() + { + return \PDO::PARAM_INT; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/BooleanType.php b/Doctrine/DBAL/Types/BooleanType.php new file mode 100644 index 0000000..8c93ef6 --- /dev/null +++ b/Doctrine/DBAL/Types/BooleanType.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL boolean to a PHP boolean. + * + * @since 2.0 + */ +class BooleanType extends Type +{ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getBooleanTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $platform->convertBooleans($value); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (bool) $value; + } + + public function getName() + { + return Type::BOOLEAN; + } + + public function getBindingType() + { + return \PDO::PARAM_BOOL; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/ConversionException.php b/Doctrine/DBAL/Types/ConversionException.php new file mode 100644 index 0000000..fdeb1b8 --- /dev/null +++ b/Doctrine/DBAL/Types/ConversionException.php @@ -0,0 +1,48 @@ +. + */ + + +/** + * Conversion Exception is thrown when the database to PHP conversion fails + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +namespace Doctrine\DBAL\Types; + +class ConversionException extends \Doctrine\DBAL\DBALException +{ + /** + * Thrown when a Database to Doctrine Type Conversion fails. + * + * @param string $value + * @param string $toType + * @return ConversionException + */ + static public function conversionFailed($value, $toType) + { + $value = (strlen($value) > 32) ? substr($value, 0, 20) . "..." : $value; + return new self('Could not convert database value "' . $value . '" to Doctrine Type ' . $toType); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/DateTimeType.php b/Doctrine/DBAL/Types/DateTimeType.php new file mode 100644 index 0000000..e99c5cb --- /dev/null +++ b/Doctrine/DBAL/Types/DateTimeType.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DATETIME/TIMESTAMP to a PHP DateTime object. + * + * @since 2.0 + */ +class DateTimeType extends Type +{ + public function getName() + { + return Type::DATETIME; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTimeTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateTimeFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $val = \DateTime::createFromFormat($platform->getDateTimeFormatString(), $value); + if (!$val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/DateTimeTzType.php b/Doctrine/DBAL/Types/DateTimeTzType.php new file mode 100644 index 0000000..ca281c2 --- /dev/null +++ b/Doctrine/DBAL/Types/DateTimeTzType.php @@ -0,0 +1,79 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * DateTime type saving additional timezone information. + * + * Caution: Databases are not necessarily experts at storing timezone related + * data of dates. First, of all the supported vendors only PostgreSQL and Oracle + * support storing Timezone data. But those two don't save the actual timezone + * attached to a DateTime instance (for example "Europe/Berlin" or "America/Montreal") + * but the current offset of them related to UTC. That means depending on daylight saving times + * or not you may get different offsets. + * + * This datatype makes only sense to use, if your application works with an offset, not + * with an actual timezone that uses transitions. Otherwise your DateTime instance + * attached with a timezone such as Europe/Berlin gets saved into the database with + * the offset and re-created from persistence with only the offset, not the original timezone + * attached. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DateTimeTzType extends Type +{ + public function getName() + { + return Type::DATETIMETZ; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTimeTzTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateTimeTzFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $val = \DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value); + if (!$val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/DateType.php b/Doctrine/DBAL/Types/DateType.php new file mode 100644 index 0000000..ed492dc --- /dev/null +++ b/Doctrine/DBAL/Types/DateType.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DATE to a PHP Date object. + * + * @since 2.0 + */ +class DateType extends Type +{ + public function getName() + { + return Type::DATE; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDateTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getDateFormatString()) : null; + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $val = \DateTime::createFromFormat('!'.$platform->getDateFormatString(), $value); + if (!$val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/DecimalType.php b/Doctrine/DBAL/Types/DecimalType.php new file mode 100644 index 0000000..90b2bda --- /dev/null +++ b/Doctrine/DBAL/Types/DecimalType.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL DECIMAL to a PHP double. + * + * @since 2.0 + */ +class DecimalType extends Type +{ + public function getName() + { + return Type::DECIMAL; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getDecimalTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (double) $value; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/FloatType.php b/Doctrine/DBAL/Types/FloatType.php new file mode 100644 index 0000000..c8f7914 --- /dev/null +++ b/Doctrine/DBAL/Types/FloatType.php @@ -0,0 +1,54 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +class FloatType extends Type +{ + public function getName() + { + return Type::FLOAT; + } + + /** + * @param array $fieldDeclaration + * @param AbstractPlatform $platform + * @return string + */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getFloatDeclarationSQL($fieldDeclaration); + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (double) $value; + } +} diff --git a/Doctrine/DBAL/Types/IntegerType.php b/Doctrine/DBAL/Types/IntegerType.php new file mode 100644 index 0000000..dd13f0a --- /dev/null +++ b/Doctrine/DBAL/Types/IntegerType.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL INT to a PHP integer. + * + * @author Roman Borschel + * @since 2.0 + */ +class IntegerType extends Type +{ + public function getName() + { + return Type::INTEGER; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getIntegerTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (int) $value; + } + + public function getBindingType() + { + return \PDO::PARAM_INT; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/ObjectType.php b/Doctrine/DBAL/Types/ObjectType.php new file mode 100644 index 0000000..aaa8477 --- /dev/null +++ b/Doctrine/DBAL/Types/ObjectType.php @@ -0,0 +1,40 @@ +getClobTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToDatabaseValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + return serialize($value); + } + + public function convertToPHPValue($value, \Doctrine\DBAL\Platforms\AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = (is_resource($value)) ? stream_get_contents($value) : $value; + $val = unserialize($value); + if ($val === false) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } + + public function getName() + { + return Type::OBJECT; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/SmallIntType.php b/Doctrine/DBAL/Types/SmallIntType.php new file mode 100644 index 0000000..5e1df69 --- /dev/null +++ b/Doctrine/DBAL/Types/SmallIntType.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps a database SMALLINT to a PHP integer. + * + * @author robo + */ +class SmallIntType extends Type +{ + public function getName() + { + return Type::SMALLINT; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getSmallIntTypeDeclarationSQL($fieldDeclaration); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (null === $value) ? null : (int) $value; + } + + public function getBindingType() + { + return \PDO::PARAM_INT; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/StringType.php b/Doctrine/DBAL/Types/StringType.php new file mode 100644 index 0000000..a813eaa --- /dev/null +++ b/Doctrine/DBAL/Types/StringType.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL VARCHAR to a PHP string. + * + * @since 2.0 + */ +class StringType extends Type +{ + /** @override */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getVarcharTypeDeclarationSQL($fieldDeclaration); + } + + /** @override */ + public function getDefaultLength(AbstractPlatform $platform) + { + return $platform->getVarcharDefaultLength(); + } + + /** @override */ + public function getName() + { + return Type::STRING; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/TextType.php b/Doctrine/DBAL/Types/TextType.php new file mode 100644 index 0000000..50b4f83 --- /dev/null +++ b/Doctrine/DBAL/Types/TextType.php @@ -0,0 +1,56 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL CLOB to a PHP string. + * + * @since 2.0 + */ +class TextType extends Type +{ + /** @override */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getClobTypeDeclarationSQL($fieldDeclaration); + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return (is_resource($value)) ? stream_get_contents($value) : $value; + } + + public function getName() + { + return Type::TEXT; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/TimeType.php b/Doctrine/DBAL/Types/TimeType.php new file mode 100644 index 0000000..66952ef --- /dev/null +++ b/Doctrine/DBAL/Types/TimeType.php @@ -0,0 +1,68 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Type that maps an SQL TIME to a PHP DateTime object. + * + * @since 2.0 + */ +class TimeType extends Type +{ + public function getName() + { + return Type::TIME; + } + + /** + * {@inheritdoc} + */ + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + { + return $platform->getTimeTypeDeclarationSQL($fieldDeclaration); + } + + /** + * {@inheritdoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return ($value !== null) + ? $value->format($platform->getTimeFormatString()) : null; + } + + /** + * {@inheritdoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $val = \DateTime::createFromFormat($platform->getTimeFormatString(), $value); + if (!$val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/Type.php b/Doctrine/DBAL/Types/Type.php new file mode 100644 index 0000000..9d1b52c --- /dev/null +++ b/Doctrine/DBAL/Types/Type.php @@ -0,0 +1,230 @@ +. + */ + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform, + Doctrine\DBAL\DBALException; + +/** + * The base class for so-called Doctrine mapping types. + * + * A Type object is obtained by calling the static {@link getType()} method. + * + * @author Roman Borschel + * @since 2.0 + */ +abstract class Type +{ + const TARRAY = 'array'; + const BIGINT = 'bigint'; + const BOOLEAN = 'boolean'; + const DATETIME = 'datetime'; + const DATETIMETZ = 'datetimetz'; + const DATE = 'date'; + const TIME = 'time'; + const DECIMAL = 'decimal'; + const INTEGER = 'integer'; + const OBJECT = 'object'; + const SMALLINT = 'smallint'; + const STRING = 'string'; + const TEXT = 'text'; + const FLOAT = 'float'; + + /** Map of already instantiated type objects. One instance per type (flyweight). */ + private static $_typeObjects = array(); + + /** The map of supported doctrine mapping types. */ + private static $_typesMap = array( + self::TARRAY => 'Doctrine\DBAL\Types\ArrayType', + self::OBJECT => 'Doctrine\DBAL\Types\ObjectType', + self::BOOLEAN => 'Doctrine\DBAL\Types\BooleanType', + self::INTEGER => 'Doctrine\DBAL\Types\IntegerType', + self::SMALLINT => 'Doctrine\DBAL\Types\SmallIntType', + self::BIGINT => 'Doctrine\DBAL\Types\BigIntType', + self::STRING => 'Doctrine\DBAL\Types\StringType', + self::TEXT => 'Doctrine\DBAL\Types\TextType', + self::DATETIME => 'Doctrine\DBAL\Types\DateTimeType', + self::DATETIMETZ => 'Doctrine\DBAL\Types\DateTimeTzType', + self::DATE => 'Doctrine\DBAL\Types\DateType', + self::TIME => 'Doctrine\DBAL\Types\TimeType', + self::DECIMAL => 'Doctrine\DBAL\Types\DecimalType', + self::FLOAT => 'Doctrine\DBAL\Types\FloatType', + ); + + /* Prevent instantiation and force use of the factory method. */ + final private function __construct() {} + + /** + * Converts a value from its PHP representation to its database representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The database representation of the value. + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * @return mixed The PHP representation of the value. + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Gets the default length of this type. + * + * @todo Needed? + */ + public function getDefaultLength(AbstractPlatform $platform) + { + return null; + } + + /** + * Gets the SQL declaration snippet for a field of this type. + * + * @param array $fieldDeclaration The field declaration. + * @param AbstractPlatform $platform The currently used database platform. + */ + abstract public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform); + + /** + * Gets the name of this type. + * + * @return string + * @todo Needed? + */ + abstract public function getName(); + + /** + * Factory method to create type instances. + * Type instances are implemented as flyweights. + * + * @static + * @throws DBALException + * @param string $name The name of the type (as returned by getName()). + * @return Doctrine\DBAL\Types\Type + */ + public static function getType($name) + { + if ( ! isset(self::$_typeObjects[$name])) { + if ( ! isset(self::$_typesMap[$name])) { + throw DBALException::unknownColumnType($name); + } + self::$_typeObjects[$name] = new self::$_typesMap[$name](); + } + + return self::$_typeObjects[$name]; + } + + /** + * Adds a custom type to the type map. + * + * @static + * @param string $name Name of the type. This should correspond to what getName() returns. + * @param string $className The class name of the custom type. + * @throws DBALException + */ + public static function addType($name, $className) + { + if (isset(self::$_typesMap[$name])) { + throw DBALException::typeExists($name); + } + + self::$_typesMap[$name] = $className; + } + + /** + * Checks if exists support for a type. + * + * @static + * @param string $name Name of the type + * @return boolean TRUE if type is supported; FALSE otherwise + */ + public static function hasType($name) + { + return isset(self::$_typesMap[$name]); + } + + /** + * Overrides an already defined type to use a different implementation. + * + * @static + * @param string $name + * @param string $className + * @throws DBALException + */ + public static function overrideType($name, $className) + { + if ( ! isset(self::$_typesMap[$name])) { + throw DBALException::typeNotFound($name); + } + + self::$_typesMap[$name] = $className; + } + + /** + * Gets the (preferred) binding type for values of this type that + * can be used when binding parameters to prepared statements. + * + * This method should return one of the PDO::PARAM_* constants, that is, one of: + * + * PDO::PARAM_BOOL + * PDO::PARAM_NULL + * PDO::PARAM_INT + * PDO::PARAM_STR + * PDO::PARAM_LOB + * + * @return integer + */ + public function getBindingType() + { + return \PDO::PARAM_STR; + } + + /** + * Get the types array map which holds all registered types and the corresponding + * type class + * + * @return array $typesMap + */ + public static function getTypesMap() + { + return self::$_typesMap; + } + + public function __toString() + { + $e = explode('\\', get_class($this)); + return str_replace('Type', '', end($e)); + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Types/VarDateTimeType.php b/Doctrine/DBAL/Types/VarDateTimeType.php new file mode 100644 index 0000000..03ea048 --- /dev/null +++ b/Doctrine/DBAL/Types/VarDateTimeType.php @@ -0,0 +1,60 @@ +. + */ + + +namespace Doctrine\DBAL\Types; + +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * Variable DateTime Type using date_create() instead of DateTime::createFromFormat() + * + * This type has performance implications as it runs twice as long as the regular + * {@see DateTimeType}, however in certain PostgreSQL configurations with + * TIMESTAMP(n) columns where n > 0 it is necessary to use this type. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class VarDateTimeType extends DateTimeType +{ + /** + * @throws ConversionException + * @param string $value + * @param AbstractPlatform $platform + * @return DateTime + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $val = date_create($value); + if (!$val) { + throw ConversionException::conversionFailed($value, $this->getName()); + } + return $val; + } +} \ No newline at end of file diff --git a/Doctrine/DBAL/Version.php b/Doctrine/DBAL/Version.php new file mode 100644 index 0000000..0ce6537 --- /dev/null +++ b/Doctrine/DBAL/Version.php @@ -0,0 +1,55 @@ +. + */ + +namespace Doctrine\DBAL; + +/** + * Class to store and retrieve the version of Doctrine + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Version +{ + /** + * Current Doctrine Version + */ + const VERSION = '2.0.0RC4-DEV'; + + /** + * Compares a Doctrine version with the current one. + * + * @param string $version Doctrine version to compare. + * @return int Returns -1 if older, 0 if it is the same, 1 if version + * passed as argument is newer. + */ + public static function compare($version) + { + $currentVersion = str_replace(' ', '', strtolower(self::VERSION)); + $version = str_replace(' ', '', $version); + + return version_compare($version, $currentVersion); + } +} diff --git a/Doctrine/ORM/AbstractQuery.php b/Doctrine/ORM/AbstractQuery.php new file mode 100644 index 0000000..562a5d6 --- /dev/null +++ b/Doctrine/ORM/AbstractQuery.php @@ -0,0 +1,603 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\DBAL\Types\Type, + Doctrine\ORM\Query\QueryException; + +/** + * Base contract for ORM queries. Base class for Query and NativeQuery. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Konsta Vesterinen + */ +abstract class AbstractQuery +{ + /* Hydration mode constants */ + /** + * Hydrates an object graph. This is the default behavior. + */ + const HYDRATE_OBJECT = 1; + /** + * Hydrates an array graph. + */ + const HYDRATE_ARRAY = 2; + /** + * Hydrates a flat, rectangular result set with scalar values. + */ + const HYDRATE_SCALAR = 3; + /** + * Hydrates a single scalar value. + */ + const HYDRATE_SINGLE_SCALAR = 4; + + /** + * @var array The parameter map of this query. + */ + protected $_params = array(); + + /** + * @var array The parameter type map of this query. + */ + protected $_paramTypes = array(); + + /** + * @var ResultSetMapping The user-specified ResultSetMapping to use. + */ + protected $_resultSetMapping; + + /** + * @var Doctrine\ORM\EntityManager The entity manager used by this query object. + */ + protected $_em; + + /** + * @var array The map of query hints. + */ + protected $_hints = array(); + + /** + * @var integer The hydration mode. + */ + protected $_hydrationMode = self::HYDRATE_OBJECT; + + /** + * The locally set cache driver used for caching result sets of this query. + * + * @var CacheDriver + */ + protected $_resultCacheDriver; + + /** + * Boolean flag for whether or not to cache the results of this query. + * + * @var boolean + */ + protected $_useResultCache; + + /** + * @var string The id to store the result cache entry under. + */ + protected $_resultCacheId; + + /** + * @var boolean Boolean value that indicates whether or not expire the result cache. + */ + protected $_expireResultCache = false; + + /** + * @var int Result Cache lifetime. + */ + protected $_resultCacheTTL; + + /** + * Initializes a new instance of a class derived from AbstractQuery. + * + * @param Doctrine\ORM\EntityManager $entityManager + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + } + + /** + * Retrieves the associated EntityManager of this Query instance. + * + * @return Doctrine\ORM\EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /** + * Frees the resources used by the query object. + * + * Resets Parameters, Parameter Types and Query Hints. + * + * @return void + */ + public function free() + { + $this->_params = array(); + $this->_paramTypes = array(); + $this->_hints = array(); + } + + /** + * Get all defined parameters. + * + * @return array The defined query parameters. + */ + public function getParameters() + { + return $this->_params; + } + + /** + * Gets a query parameter. + * + * @param mixed $key The key (index or name) of the bound parameter. + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) + { + return isset($this->_params[$key]) ? $this->_params[$key] : null; + } + + /** + * Gets the SQL query that corresponds to this query object. + * The returned SQL syntax depends on the connection driver that is used + * by this query object at the time of this method call. + * + * @return string SQL query + */ + abstract public function getSQL(); + + /** + * Sets a query parameter. + * + * @param string|integer $key The parameter position or name. + * @param mixed $value The parameter value. + * @param string $type The parameter type. If specified, the given value will be run through + * the type conversion of this type. This is usually not needed for + * strings and numeric types. + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function setParameter($key, $value, $type = null) + { + if ($type !== null) { + $this->_paramTypes[$key] = $type; + } + $this->_params[$key] = $value; + return $this; + } + + /** + * Sets a collection of query parameters. + * + * @param array $params + * @param array $types + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function setParameters(array $params, array $types = array()) + { + foreach ($params as $key => $value) { + if (isset($types[$key])) { + $this->setParameter($key, $value, $types[$key]); + } else { + $this->setParameter($key, $value); + } + } + return $this; + } + + /** + * Sets the ResultSetMapping that should be used for hydration. + * + * @param ResultSetMapping $rsm + * @return Doctrine\ORM\AbstractQuery + */ + public function setResultSetMapping(Query\ResultSetMapping $rsm) + { + $this->_resultSetMapping = $rsm; + return $this; + } + + /** + * Defines a cache driver to be used for caching result sets. + * + * @param Doctrine\Common\Cache\Cache $driver Cache driver + * @return Doctrine\ORM\AbstractQuery + */ + public function setResultCacheDriver($resultCacheDriver = null) + { + if ($resultCacheDriver !== null && ! ($resultCacheDriver instanceof \Doctrine\Common\Cache\Cache)) { + throw ORMException::invalidResultCacheDriver(); + } + $this->_resultCacheDriver = $resultCacheDriver; + if ($resultCacheDriver) { + $this->_useResultCache = true; + } + return $this; + } + + /** + * Returns the cache driver used for caching result sets. + * + * @return Doctrine\Common\Cache\Cache Cache driver + */ + public function getResultCacheDriver() + { + if ($this->_resultCacheDriver) { + return $this->_resultCacheDriver; + } else { + return $this->_em->getConfiguration()->getResultCacheImpl(); + } + } + + /** + * Set whether or not to cache the results of this query and if so, for + * how long and which ID to use for the cache entry. + * + * @param boolean $bool + * @param integer $timeToLive + * @param string $resultCacheId + * @return This query instance. + */ + public function useResultCache($bool, $timeToLive = null, $resultCacheId = null) + { + $this->_useResultCache = $bool; + if ($timeToLive) { + $this->setResultCacheLifetime($timeToLive); + } + if ($resultCacheId) { + $this->_resultCacheId = $resultCacheId; + } + return $this; + } + + /** + * Defines how long the result cache will be active before expire. + * + * @param integer $timeToLive How long the cache entry is valid. + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function setResultCacheLifetime($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + + $this->_resultCacheTTL = $timeToLive; + return $this; + } + + /** + * Retrieves the lifetime of resultset cache. + * + * @return integer + */ + public function getResultCacheLifetime() + { + return $this->_resultCacheTTL; + } + + /** + * Defines if the result cache is active or not. + * + * @param boolean $expire Whether or not to force resultset cache expiration. + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function expireResultCache($expire = true) + { + $this->_expireResultCache = $expire; + return $this; + } + + /** + * Retrieves if the resultset cache is active or not. + * + * @return boolean + */ + public function getExpireResultCache() + { + return $this->_expireResultCache; + } + + /** + * Defines the processing mode to be used during hydration / result set transformation. + * + * @param integer $hydrationMode Doctrine processing mode to be used during hydration process. + * One of the Query::HYDRATE_* constants. + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function setHydrationMode($hydrationMode) + { + $this->_hydrationMode = $hydrationMode; + return $this; + } + + /** + * Gets the hydration mode currently used by the query. + * + * @return integer + */ + public function getHydrationMode() + { + return $this->_hydrationMode; + } + + /** + * Gets the list of results for the query. + * + * Alias for execute(array(), $hydrationMode = HYDRATE_OBJECT). + * + * @return array + */ + public function getResult($hydrationMode = self::HYDRATE_OBJECT) + { + return $this->execute(array(), $hydrationMode); + } + + /** + * Gets the array of results for the query. + * + * Alias for execute(array(), HYDRATE_ARRAY). + * + * @return array + */ + public function getArrayResult() + { + return $this->execute(array(), self::HYDRATE_ARRAY); + } + + /** + * Gets the scalar results for the query. + * + * Alias for execute(array(), HYDRATE_SCALAR). + * + * @return array + */ + public function getScalarResult() + { + return $this->execute(array(), self::HYDRATE_SCALAR); + } + + /** + * Gets the single result of the query. + * + * Enforces the presence as well as the uniqueness of the result. + * + * If the result is not unique, a NonUniqueResultException is thrown. + * If there is no result, a NoResultException is thrown. + * + * @param integer $hydrationMode + * @return mixed + * @throws NonUniqueResultException If the query result is not unique. + * @throws NoResultException If the query returned no result. + */ + public function getSingleResult($hydrationMode = null) + { + $result = $this->execute(array(), $hydrationMode); + + if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { + throw new NoResultException; + } + + if (is_array($result)) { + if (count($result) > 1) { + throw new NonUniqueResultException; + } + return array_shift($result); + } + + return $result; + } + + /** + * Gets the single scalar result of the query. + * + * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR). + * + * @return mixed + * @throws QueryException If the query result is not unique. + */ + public function getSingleScalarResult() + { + return $this->getSingleResult(self::HYDRATE_SINGLE_SCALAR); + } + + /** + * Sets a query hint. If the hint name is not recognized, it is silently ignored. + * + * @param string $name The name of the hint. + * @param mixed $value The value of the hint. + * @return Doctrine\ORM\AbstractQuery + */ + public function setHint($name, $value) + { + $this->_hints[$name] = $value; + return $this; + } + + /** + * Gets the value of a query hint. If the hint name is not recognized, FALSE is returned. + * + * @param string $name The name of the hint. + * @return mixed The value of the hint or FALSE, if the hint name is not recognized. + */ + public function getHint($name) + { + return isset($this->_hints[$name]) ? $this->_hints[$name] : false; + } + + /** + * Executes the query and returns an IterableResult that can be used to incrementally + * iterate over the result. + * + * @param array $params The query parameters. + * @param integer $hydrationMode The hydration mode to use. + * @return IterableResult + */ + public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) + { + return $this->_em->newHydrator($this->_hydrationMode)->iterate( + $this->_doExecute($params, $hydrationMode), $this->_resultSetMapping, $this->_hints + ); + } + + /** + * Executes the query. + * + * @param string $params Any additional query parameters. + * @param integer $hydrationMode Processing mode to be used during the hydration process. + * @return mixed + */ + public function execute($params = array(), $hydrationMode = null) + { + // If there are still pending insertions in the UnitOfWork we need to flush + // in order to guarantee a correct result. + //TODO: Think this over. Its tricky. Not doing this can lead to strange results + // potentially, but doing it could result in endless loops when querying during + // a flush, i.e. inside an event listener. + if ($this->_em->getUnitOfWork()->hasPendingInsertions()) { + $this->_em->flush(); + } + + if ($hydrationMode !== null) { + $this->setHydrationMode($hydrationMode); + } + + if ($params) { + $this->setParameters($params); + } + + if (isset($this->_params[0])) { + throw QueryException::invalidParameterPosition(0); + } + + // Check result cache + if ($this->_useResultCache && $cacheDriver = $this->getResultCacheDriver()) { + $id = $this->_getResultCacheId(); + $cached = $this->_expireResultCache ? false : $cacheDriver->fetch($id); + + if ($cached === false) { + // Cache miss. + $stmt = $this->_doExecute(); + + $result = $this->_em->getHydrator($this->_hydrationMode)->hydrateAll( + $stmt, $this->_resultSetMapping, $this->_hints + ); + + $cacheDriver->save($id, $result, $this->_resultCacheTTL); + + return $result; + } else { + // Cache hit. + return $cached; + } + } + + $stmt = $this->_doExecute(); + + if (is_numeric($stmt)) { + return $stmt; + } + + return $this->_em->getHydrator($this->_hydrationMode)->hydrateAll( + $stmt, $this->_resultSetMapping, $this->_hints + ); + } + + /** + * Set the result cache id to use to store the result set cache entry. + * If this is not explicitely set by the developer then a hash is automatically + * generated for you. + * + * @param string $id + * @return Doctrine\ORM\AbstractQuery This query instance. + */ + public function setResultCacheId($id) + { + $this->_resultCacheId = $id; + return $this; + } + + /** + * Get the result cache id to use to store the result set cache entry. + * Will return the configured id if it exists otherwise a hash will be + * automatically generated for you. + * + * @return string $id + */ + protected function _getResultCacheId() + { + if ($this->_resultCacheId) { + return $this->_resultCacheId; + } else { + $params = $this->_params; + foreach ($params AS $key => $value) { + if (is_object($value) && $this->_em->getMetadataFactory()->hasMetadataFor(get_class($value))) { + if ($this->_em->getUnitOfWork()->getEntityState($value) == UnitOfWork::STATE_MANAGED) { + $idValues = $this->_em->getUnitOfWork()->getEntityIdentifier($value); + } else { + $class = $this->_em->getClassMetadata(get_class($value)); + $idValues = $class->getIdentifierValues($value); + } + $params[$key] = $idValues; + } + } + + $sql = $this->getSql(); + ksort($this->_hints); + return md5(implode(";", (array)$sql) . var_export($params, true) . + var_export($this->_hints, true)."&hydrationMode=".$this->_hydrationMode); + } + } + + /** + * Executes the query and returns a the resulting Statement object. + * + * @return Doctrine\DBAL\Driver\Statement The executed database statement that holds the results. + */ + abstract protected function _doExecute(); + + /** + * Cleanup Query resource when clone is called. + * + * @return void + */ + public function __clone() + { + $this->_params = array(); + $this->_paramTypes = array(); + $this->_hints = array(); + } +} diff --git a/Doctrine/ORM/Configuration.php b/Doctrine/ORM/Configuration.php new file mode 100644 index 0000000..0bc206d --- /dev/null +++ b/Doctrine/ORM/Configuration.php @@ -0,0 +1,486 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\Common\Cache\Cache, + Doctrine\ORM\Mapping\Driver\Driver; + +/** + * Configuration container for all configuration options of Doctrine. + * It combines all configuration options from DBAL & ORM. + * + * @since 2.0 + * @internal When adding a new configuration option just write a getter/setter pair. + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Configuration extends \Doctrine\DBAL\Configuration +{ + /** + * Sets the directory where Doctrine generates any necessary proxy class files. + * + * @param string $dir + */ + public function setProxyDir($dir) + { + $this->_attributes['proxyDir'] = $dir; + } + + /** + * Gets the directory where Doctrine generates any necessary proxy class files. + * + * @return string + */ + public function getProxyDir() + { + return isset($this->_attributes['proxyDir']) ? + $this->_attributes['proxyDir'] : null; + } + + /** + * Gets a boolean flag that indicates whether proxy classes should always be regenerated + * during each script execution. + * + * @return boolean + */ + public function getAutoGenerateProxyClasses() + { + return isset($this->_attributes['autoGenerateProxyClasses']) ? + $this->_attributes['autoGenerateProxyClasses'] : true; + } + + /** + * Sets a boolean flag that indicates whether proxy classes should always be regenerated + * during each script execution. + * + * @param boolean $bool + */ + public function setAutoGenerateProxyClasses($bool) + { + $this->_attributes['autoGenerateProxyClasses'] = $bool; + } + + /** + * Gets the namespace where proxy classes reside. + * + * @return string + */ + public function getProxyNamespace() + { + return isset($this->_attributes['proxyNamespace']) ? + $this->_attributes['proxyNamespace'] : null; + } + + /** + * Sets the namespace where proxy classes reside. + * + * @param string $ns + */ + public function setProxyNamespace($ns) + { + $this->_attributes['proxyNamespace'] = $ns; + } + + /** + * Sets the cache driver implementation that is used for metadata caching. + * + * @param Driver $driverImpl + * @todo Force parameter to be a Closure to ensure lazy evaluation + * (as soon as a metadata cache is in effect, the driver never needs to initialize). + */ + public function setMetadataDriverImpl(Driver $driverImpl) + { + $this->_attributes['metadataDriverImpl'] = $driverImpl; + } + + /** + * Add a new default annotation driver with a correctly configured annotation reader. + * + * @param array $paths + * @return Mapping\Driver\AnnotationDriver + */ + public function newDefaultAnnotationDriver($paths = array()) + { + $reader = new \Doctrine\Common\Annotations\AnnotationReader(); + $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); + + return new \Doctrine\ORM\Mapping\Driver\AnnotationDriver($reader, (array)$paths); + } + + /** + * Adds a namespace under a certain alias. + * + * @param string $alias + * @param string $namespace + */ + public function addEntityNamespace($alias, $namespace) + { + $this->_attributes['entityNamespaces'][$alias] = $namespace; + } + + /** + * Resolves a registered namespace alias to the full namespace. + * + * @param string $entityNamespaceAlias + * @return string + * @throws MappingException + */ + public function getEntityNamespace($entityNamespaceAlias) + { + if ( ! isset($this->_attributes['entityNamespaces'][$entityNamespaceAlias])) { + throw ORMException::unknownEntityNamespace($entityNamespaceAlias); + } + + return trim($this->_attributes['entityNamespaces'][$entityNamespaceAlias], '\\'); + } + + /** + * Set the entity alias map + * + * @param array $entityAliasMap + * @return void + */ + public function setEntityNamespaces(array $entityNamespaces) + { + $this->_attributes['entityNamespaces'] = $entityNamespaces; + } + + /** + * Gets the cache driver implementation that is used for the mapping metadata. + * + * @throws ORMException + * @return Mapping\Driver\Driver + */ + public function getMetadataDriverImpl() + { + return isset($this->_attributes['metadataDriverImpl']) ? + $this->_attributes['metadataDriverImpl'] : null; + } + + /** + * Gets the cache driver implementation that is used for query result caching. + * + * @return \Doctrine\Common\Cache\Cache + */ + public function getResultCacheImpl() + { + return isset($this->_attributes['resultCacheImpl']) ? + $this->_attributes['resultCacheImpl'] : null; + } + + /** + * Sets the cache driver implementation that is used for query result caching. + * + * @param \Doctrine\Common\Cache\Cache $cacheImpl + */ + public function setResultCacheImpl(Cache $cacheImpl) + { + $this->_attributes['resultCacheImpl'] = $cacheImpl; + } + + /** + * Gets the cache driver implementation that is used for the query cache (SQL cache). + * + * @return \Doctrine\Common\Cache\Cache + */ + public function getQueryCacheImpl() + { + return isset($this->_attributes['queryCacheImpl']) ? + $this->_attributes['queryCacheImpl'] : null; + } + + /** + * Sets the cache driver implementation that is used for the query cache (SQL cache). + * + * @param \Doctrine\Common\Cache\Cache $cacheImpl + */ + public function setQueryCacheImpl(Cache $cacheImpl) + { + $this->_attributes['queryCacheImpl'] = $cacheImpl; + } + + /** + * Gets the cache driver implementation that is used for metadata caching. + * + * @return \Doctrine\Common\Cache\Cache + */ + public function getMetadataCacheImpl() + { + return isset($this->_attributes['metadataCacheImpl']) ? + $this->_attributes['metadataCacheImpl'] : null; + } + + /** + * Sets the cache driver implementation that is used for metadata caching. + * + * @param \Doctrine\Common\Cache\Cache $cacheImpl + */ + public function setMetadataCacheImpl(Cache $cacheImpl) + { + $this->_attributes['metadataCacheImpl'] = $cacheImpl; + } + + /** + * Adds a named DQL query to the configuration. + * + * @param string $name The name of the query. + * @param string $dql The DQL query string. + */ + public function addNamedQuery($name, $dql) + { + $this->_attributes['namedQueries'][$name] = $dql; + } + + /** + * Gets a previously registered named DQL query. + * + * @param string $name The name of the query. + * @return string The DQL query. + */ + public function getNamedQuery($name) + { + if ( ! isset($this->_attributes['namedQueries'][$name])) { + throw ORMException::namedQueryNotFound($name); + } + return $this->_attributes['namedQueries'][$name]; + } + + /** + * Adds a named native query to the configuration. + * + * @param string $name The name of the query. + * @param string $sql The native SQL query string. + * @param ResultSetMapping $rsm The ResultSetMapping used for the results of the SQL query. + */ + public function addNamedNativeQuery($name, $sql, Query\ResultSetMapping $rsm) + { + $this->_attributes['namedNativeQueries'][$name] = array($sql, $rsm); + } + + /** + * Gets the components of a previously registered named native query. + * + * @param string $name The name of the query. + * @return array A tuple with the first element being the SQL string and the second + * element being the ResultSetMapping. + */ + public function getNamedNativeQuery($name) + { + if ( ! isset($this->_attributes['namedNativeQueries'][$name])) { + throw ORMException::namedNativeQueryNotFound($name); + } + return $this->_attributes['namedNativeQueries'][$name]; + } + + /** + * Ensures that this Configuration instance contains settings that are + * suitable for a production environment. + * + * @throws ORMException If a configuration setting has a value that is not + * suitable for a production environment. + */ + public function ensureProductionSettings() + { + if ( !$this->getQueryCacheImpl()) { + throw ORMException::queryCacheNotConfigured(); + } + if ( !$this->getMetadataCacheImpl()) { + throw ORMException::metadataCacheNotConfigured(); + } + if ($this->getAutoGenerateProxyClasses()) { + throw ORMException::proxyClassesAlwaysRegenerating(); + } + } + + /** + * Registers a custom DQL function that produces a string value. + * Such a function can then be used in any DQL statement in any place where string + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param string $name + * @param string $className + */ + public function addCustomStringFunction($name, $className) + { + $this->_attributes['customStringFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom string DQL function. + * + * @param string $name + * @return string + */ + public function getCustomStringFunction($name) + { + $name = strtolower($name); + return isset($this->_attributes['customStringFunctions'][$name]) ? + $this->_attributes['customStringFunctions'][$name] : null; + } + + /** + * Sets a map of custom DQL string functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added string functions are discarded. + * + * @param array $functions The map of custom DQL string functions. + */ + public function setCustomStringFunctions(array $functions) + { + $this->_attributes['customStringFunctions'] = array_change_key_case($functions); + } + + /** + * Registers a custom DQL function that produces a numeric value. + * Such a function can then be used in any DQL statement in any place where numeric + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param string $name + * @param string $className + */ + public function addCustomNumericFunction($name, $className) + { + $this->_attributes['customNumericFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom numeric DQL function. + * + * @param string $name + * @return string + */ + public function getCustomNumericFunction($name) + { + $name = strtolower($name); + return isset($this->_attributes['customNumericFunctions'][$name]) ? + $this->_attributes['customNumericFunctions'][$name] : null; + } + + /** + * Sets a map of custom DQL numeric functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added numeric functions are discarded. + * + * @param array $functions The map of custom DQL numeric functions. + */ + public function setCustomNumericFunctions(array $functions) + { + $this->_attributes['customNumericFunctions'] = array_change_key_case($functions); + } + + /** + * Registers a custom DQL function that produces a date/time value. + * Such a function can then be used in any DQL statement in any place where date/time + * functions are allowed. + * + * DQL function names are case-insensitive. + * + * @param string $name + * @param string $className + */ + public function addCustomDatetimeFunction($name, $className) + { + $this->_attributes['customDatetimeFunctions'][strtolower($name)] = $className; + } + + /** + * Gets the implementation class name of a registered custom date/time DQL function. + * + * @param string $name + * @return string + */ + public function getCustomDatetimeFunction($name) + { + $name = strtolower($name); + return isset($this->_attributes['customDatetimeFunctions'][$name]) ? + $this->_attributes['customDatetimeFunctions'][$name] : null; + } + + /** + * Sets a map of custom DQL date/time functions. + * + * Keys must be function names and values the FQCN of the implementing class. + * The function names will be case-insensitive in DQL. + * + * Any previously added date/time functions are discarded. + * + * @param array $functions The map of custom DQL date/time functions. + */ + public function setCustomDatetimeFunctions(array $functions) + { + $this->_attributes['customDatetimeFunctions'] = array_change_key_case($functions); + } + + /** + * Get the hydrator class for the given hydration mode name. + * + * @param string $modeName The hydration mode name. + * @return string $hydrator The hydrator class name. + */ + public function getCustomHydrationMode($modeName) + { + return isset($this->_attributes['customHydrationModes'][$modeName]) ? + $this->_attributes['customHydrationModes'][$modeName] : null; + } + + /** + * Add a custom hydration mode. + * + * @param string $modeName The hydration mode name. + * @param string $hydrator The hydrator class name. + */ + public function addCustomHydrationMode($modeName, $hydrator) + { + $this->_attributes['customHydrationModes'][$modeName] = $hydrator; + } + + /** + * Set a class metadata factory. + * + * @param string $cmf + */ + public function setClassMetadataFactoryName($cmfName) + { + $this->_attributes['classMetadataFactoryName'] = $cmfName; + } + + /** + * @return string + */ + public function getClassMetadataFactoryName() + { + if (!isset($this->_attributes['classMetadataFactoryName'])) { + $this->_attributes['classMetadataFactoryName'] = 'Doctrine\ORM\Mapping\ClassMetadataFactory'; + } + return $this->_attributes['classMetadataFactoryName']; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/EntityManager.php b/Doctrine/ORM/EntityManager.php new file mode 100644 index 0000000..3af9cf3 --- /dev/null +++ b/Doctrine/ORM/EntityManager.php @@ -0,0 +1,729 @@ +. + */ + +namespace Doctrine\ORM; + +use Closure, Exception, + Doctrine\Common\EventManager, + Doctrine\DBAL\Connection, + Doctrine\DBAL\LockMode, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\Mapping\ClassMetadataFactory, + Doctrine\ORM\Query\ResultSetMapping, + Doctrine\ORM\Proxy\ProxyFactory; + +/** + * The EntityManager is the central access point to ORM functionality. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EntityManager +{ + /** + * The used Configuration. + * + * @var Doctrine\ORM\Configuration + */ + private $config; + + /** + * The database connection used by the EntityManager. + * + * @var Doctrine\DBAL\Connection + */ + private $conn; + + /** + * The metadata factory, used to retrieve the ORM metadata of entity classes. + * + * @var Doctrine\ORM\Mapping\ClassMetadataFactory + */ + private $metadataFactory; + + /** + * The EntityRepository instances. + * + * @var array + */ + private $repositories = array(); + + /** + * The UnitOfWork used to coordinate object-level transactions. + * + * @var Doctrine\ORM\UnitOfWork + */ + private $unitOfWork; + + /** + * The event manager that is the central point of the event system. + * + * @var Doctrine\Common\EventManager + */ + private $eventManager; + + /** + * The maintained (cached) hydrators. One instance per type. + * + * @var array + */ + private $hydrators = array(); + + /** + * The proxy factory used to create dynamic proxies. + * + * @var Doctrine\ORM\Proxy\ProxyFactory + */ + private $proxyFactory; + + /** + * @var ExpressionBuilder The expression builder instance used to generate query expressions. + */ + private $expressionBuilder; + + /** + * Whether the EntityManager is closed or not. + */ + private $closed = false; + + /** + * Creates a new EntityManager that operates on the given database connection + * and uses the given Configuration and EventManager implementations. + * + * @param Doctrine\DBAL\Connection $conn + * @param Doctrine\ORM\Configuration $config + * @param Doctrine\Common\EventManager $eventManager + */ + protected function __construct(Connection $conn, Configuration $config, EventManager $eventManager) + { + $this->conn = $conn; + $this->config = $config; + $this->eventManager = $eventManager; + + $metadataFactoryClassName = $config->getClassMetadataFactoryName(); + $this->metadataFactory = new $metadataFactoryClassName; + $this->metadataFactory->setEntityManager($this); + $this->metadataFactory->setCacheDriver($this->config->getMetadataCacheImpl()); + + $this->unitOfWork = new UnitOfWork($this); + $this->proxyFactory = new ProxyFactory($this, + $config->getProxyDir(), + $config->getProxyNamespace(), + $config->getAutoGenerateProxyClasses()); + } + + /** + * Gets the database connection object used by the EntityManager. + * + * @return Doctrine\DBAL\Connection + */ + public function getConnection() + { + return $this->conn; + } + + /** + * Gets the metadata factory used to gather the metadata of classes. + * + * @return Doctrine\ORM\Mapping\ClassMetadataFactory + */ + public function getMetadataFactory() + { + return $this->metadataFactory; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * + * Example: + * + * + * $qb = $em->createQueryBuilder(); + * $expr = $em->getExpressionBuilder(); + * $qb->select('u')->from('User', 'u') + * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); + * + * + * @return ExpressionBuilder + */ + public function getExpressionBuilder() + { + if ($this->expressionBuilder === null) { + $this->expressionBuilder = new Query\Expr; + } + return $this->expressionBuilder; + } + + /** + * Starts a transaction on the underlying database connection. + * + * @deprecated Use {@link getConnection}.beginTransaction(). + */ + public function beginTransaction() + { + $this->conn->beginTransaction(); + } + + /** + * Executes a function in a transaction. + * + * The function gets passed this EntityManager instance as an (optional) parameter. + * + * {@link flush} is invoked prior to transaction commit. + * + * If an exception occurs during execution of the function or flushing or transaction commit, + * the transaction is rolled back, the EntityManager closed and the exception re-thrown. + * + * @param Closure $func The function to execute transactionally. + */ + public function transactional(Closure $func) + { + $this->conn->beginTransaction(); + try { + $func($this); + $this->flush(); + $this->conn->commit(); + } catch (Exception $e) { + $this->close(); + $this->conn->rollback(); + throw $e; + } + } + + /** + * Commits a transaction on the underlying database connection. + * + * @deprecated Use {@link getConnection}.commit(). + */ + public function commit() + { + $this->conn->commit(); + } + + /** + * Performs a rollback on the underlying database connection. + * + * @deprecated Use {@link getConnection}.rollback(). + */ + public function rollback() + { + $this->conn->rollback(); + } + + /** + * Returns the ORM metadata descriptor for a class. + * + * The class name must be the fully-qualified class name without a leading backslash + * (as it is returned by get_class($obj)) or an aliased class name. + * + * Examples: + * MyProject\Domain\User + * sales:PriceRequest + * + * @return Doctrine\ORM\Mapping\ClassMetadata + * @internal Performance-sensitive method. + */ + public function getClassMetadata($className) + { + return $this->metadataFactory->getMetadataFor($className); + } + + /** + * Creates a new Query object. + * + * @param string The DQL string. + * @return Doctrine\ORM\Query + */ + public function createQuery($dql = "") + { + $query = new Query($this); + if ( ! empty($dql)) { + $query->setDql($dql); + } + return $query; + } + + /** + * Creates a Query from a named query. + * + * @param string $name + * @return Doctrine\ORM\Query + */ + public function createNamedQuery($name) + { + return $this->createQuery($this->config->getNamedQuery($name)); + } + + /** + * Creates a native SQL query. + * + * @param string $sql + * @param ResultSetMapping $rsm The ResultSetMapping to use. + * @return NativeQuery + */ + public function createNativeQuery($sql, ResultSetMapping $rsm) + { + $query = new NativeQuery($this); + $query->setSql($sql); + $query->setResultSetMapping($rsm); + return $query; + } + + /** + * Creates a NativeQuery from a named native query. + * + * @param string $name + * @return Doctrine\ORM\NativeQuery + */ + public function createNamedNativeQuery($name) + { + list($sql, $rsm) = $this->config->getNamedNativeQuery($name); + return $this->createNativeQuery($sql, $rsm); + } + + /** + * Create a QueryBuilder instance + * + * @return QueryBuilder $qb + */ + public function createQueryBuilder() + { + return new QueryBuilder($this); + } + + /** + * Flushes all changes to objects that have been queued up to now to the database. + * This effectively synchronizes the in-memory state of managed objects with the + * database. + * + * @throws Doctrine\ORM\OptimisticLockException If a version check on an entity that + * makes use of optimistic locking fails. + */ + public function flush() + { + $this->errorIfClosed(); + $this->unitOfWork->commit(); + } + + /** + * Finds an Entity by its identifier. + * + * This is just a convenient shortcut for getRepository($entityName)->find($id). + * + * @param string $entityName + * @param mixed $identifier + * @param int $lockMode + * @param int $lockVersion + * @return object + */ + public function find($entityName, $identifier, $lockMode = LockMode::NONE, $lockVersion = null) + { + return $this->getRepository($entityName)->find($identifier, $lockMode, $lockVersion); + } + + /** + * Gets a reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * @param string $entityName The name of the entity type. + * @param mixed $identifier The entity identifier. + * @return object The entity reference. + */ + public function getReference($entityName, $identifier) + { + $class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\')); + + // Check identity map first, if its already in there just return it. + if ($entity = $this->unitOfWork->tryGetById($identifier, $class->rootEntityName)) { + return $entity; + } + if ($class->subClasses) { + $entity = $this->find($entityName, $identifier); + } else { + if ( ! is_array($identifier)) { + $identifier = array($class->identifier[0] => $identifier); + } + $entity = $this->proxyFactory->getProxy($class->name, $identifier); + $this->unitOfWork->registerManaged($entity, $identifier, array()); + } + + return $entity; + } + + /** + * Gets a partial reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * The returned reference may be a partial object if the entity is not yet loaded/managed. + * If it is a partial object it will not initialize the rest of the entity state on access. + * Thus you can only ever safely access the identifier of an entity obtained through + * this method. + * + * The use-cases for partial references involve maintaining bidirectional associations + * without loading one side of the association or to update an entity without loading it. + * Note, however, that in the latter case the original (persistent) entity data will + * never be visible to the application (especially not event listeners) as it will + * never be loaded in the first place. + * + * @param string $entityName The name of the entity type. + * @param mixed $identifier The entity identifier. + * @return object The (partial) entity reference. + */ + public function getPartialReference($entityName, $identifier) + { + $class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\')); + + // Check identity map first, if its already in there just return it. + if ($entity = $this->unitOfWork->tryGetById($identifier, $class->rootEntityName)) { + return $entity; + } + if ( ! is_array($identifier)) { + $identifier = array($class->identifier[0] => $identifier); + } + + $entity = $class->newInstance(); + $class->setIdentifierValues($entity, $identifier); + $this->unitOfWork->registerManaged($entity, $identifier, array()); + + return $entity; + } + + /** + * Clears the EntityManager. All entities that are currently managed + * by this EntityManager become detached. + * + * @param string $entityName + */ + public function clear($entityName = null) + { + if ($entityName === null) { + $this->unitOfWork->clear(); + } else { + //TODO + throw new ORMException("EntityManager#clear(\$entityName) not yet implemented."); + } + } + + /** + * Closes the EntityManager. All entities that are currently managed + * by this EntityManager become detached. The EntityManager may no longer + * be used after it is closed. + */ + public function close() + { + $this->clear(); + $this->closed = true; + } + + /** + * Tells the EntityManager to make an instance managed and persistent. + * + * The entity will be entered into the database at or before transaction + * commit or as a result of the flush operation. + * + * NOTE: The persist operation always considers entities that are not yet known to + * this EntityManager as NEW. Do not pass detached entities to the persist operation. + * + * @param object $object The instance to make managed and persistent. + */ + public function persist($entity) + { + if ( ! is_object($entity)) { + throw new \InvalidArgumentException(gettype($entity)); + } + $this->errorIfClosed(); + $this->unitOfWork->persist($entity); + } + + /** + * Removes an entity instance. + * + * A removed entity will be removed from the database at or before transaction commit + * or as a result of the flush operation. + * + * @param object $entity The entity instance to remove. + */ + public function remove($entity) + { + if ( ! is_object($entity)) { + throw new \InvalidArgumentException(gettype($entity)); + } + $this->errorIfClosed(); + $this->unitOfWork->remove($entity); + } + + /** + * Refreshes the persistent state of an entity from the database, + * overriding any local changes that have not yet been persisted. + * + * @param object $entity The entity to refresh. + */ + public function refresh($entity) + { + if ( ! is_object($entity)) { + throw new \InvalidArgumentException(gettype($entity)); + } + $this->errorIfClosed(); + $this->unitOfWork->refresh($entity); + } + + /** + * Detaches an entity from the EntityManager, causing a managed entity to + * become detached. Unflushed changes made to the entity if any + * (including removal of the entity), will not be synchronized to the database. + * Entities which previously referenced the detached entity will continue to + * reference it. + * + * @param object $entity The entity to detach. + */ + public function detach($entity) + { + if ( ! is_object($entity)) { + throw new \InvalidArgumentException(gettype($entity)); + } + $this->unitOfWork->detach($entity); + } + + /** + * Merges the state of a detached entity into the persistence context + * of this EntityManager and returns the managed copy of the entity. + * The entity passed to merge will not become associated/managed with this EntityManager. + * + * @param object $entity The detached entity to merge into the persistence context. + * @return object The managed copy of the entity. + */ + public function merge($entity) + { + if ( ! is_object($entity)) { + throw new \InvalidArgumentException(gettype($entity)); + } + $this->errorIfClosed(); + return $this->unitOfWork->merge($entity); + } + + /** + * Creates a copy of the given entity. Can create a shallow or a deep copy. + * + * @param object $entity The entity to copy. + * @return object The new entity. + * @todo Implementation need. This is necessary since $e2 = clone $e1; throws an E_FATAL when access anything on $e: + * Fatal error: Maximum function nesting level of '100' reached, aborting! + */ + public function copy($entity, $deep = false) + { + throw new \BadMethodCallException("Not implemented."); + } + + /** + * Acquire a lock on the given entity. + * + * @param object $entity + * @param int $lockMode + * @param int $lockVersion + * @throws OptimisticLockException + * @throws PessimisticLockException + */ + public function lock($entity, $lockMode, $lockVersion = null) + { + $this->unitOfWork->lock($entity, $lockMode, $lockVersion); + } + + /** + * Gets the repository for an entity class. + * + * @param string $entityName The name of the entity. + * @return EntityRepository The repository class. + */ + public function getRepository($entityName) + { + $entityName = ltrim($entityName, '\\'); + if (isset($this->repositories[$entityName])) { + return $this->repositories[$entityName]; + } + + $metadata = $this->getClassMetadata($entityName); + $customRepositoryClassName = $metadata->customRepositoryClassName; + + if ($customRepositoryClassName !== null) { + $repository = new $customRepositoryClassName($this, $metadata); + } else { + $repository = new EntityRepository($this, $metadata); + } + + $this->repositories[$entityName] = $repository; + + return $repository; + } + + /** + * Determines whether an entity instance is managed in this EntityManager. + * + * @param object $entity + * @return boolean TRUE if this EntityManager currently manages the given entity, FALSE otherwise. + */ + public function contains($entity) + { + return $this->unitOfWork->isScheduledForInsert($entity) || + $this->unitOfWork->isInIdentityMap($entity) && + ! $this->unitOfWork->isScheduledForDelete($entity); + } + + /** + * Gets the EventManager used by the EntityManager. + * + * @return Doctrine\Common\EventManager + */ + public function getEventManager() + { + return $this->eventManager; + } + + /** + * Gets the Configuration used by the EntityManager. + * + * @return Doctrine\ORM\Configuration + */ + public function getConfiguration() + { + return $this->config; + } + + /** + * Throws an exception if the EntityManager is closed or currently not active. + * + * @throws ORMException If the EntityManager is closed. + */ + private function errorIfClosed() + { + if ($this->closed) { + throw ORMException::entityManagerClosed(); + } + } + + /** + * Check if the Entity manager is open or closed. + * + * @return bool + */ + public function isOpen() + { + return (!$this->closed); + } + + /** + * Gets the UnitOfWork used by the EntityManager to coordinate operations. + * + * @return Doctrine\ORM\UnitOfWork + */ + public function getUnitOfWork() + { + return $this->unitOfWork; + } + + /** + * Gets a hydrator for the given hydration mode. + * + * This method caches the hydrator instances which is used for all queries that don't + * selectively iterate over the result. + * + * @param int $hydrationMode + * @return Doctrine\ORM\Internal\Hydration\AbstractHydrator + */ + public function getHydrator($hydrationMode) + { + if ( ! isset($this->hydrators[$hydrationMode])) { + $this->hydrators[$hydrationMode] = $this->newHydrator($hydrationMode); + } + + return $this->hydrators[$hydrationMode]; + } + + /** + * Create a new instance for the given hydration mode. + * + * @param int $hydrationMode + * @return Doctrine\ORM\Internal\Hydration\AbstractHydrator + */ + public function newHydrator($hydrationMode) + { + switch ($hydrationMode) { + case Query::HYDRATE_OBJECT: + $hydrator = new Internal\Hydration\ObjectHydrator($this); + break; + case Query::HYDRATE_ARRAY: + $hydrator = new Internal\Hydration\ArrayHydrator($this); + break; + case Query::HYDRATE_SCALAR: + $hydrator = new Internal\Hydration\ScalarHydrator($this); + break; + case Query::HYDRATE_SINGLE_SCALAR: + $hydrator = new Internal\Hydration\SingleScalarHydrator($this); + break; + default: + if ($class = $this->config->getCustomHydrationMode($hydrationMode)) { + $hydrator = new $class($this); + break; + } + throw ORMException::invalidHydrationMode($hydrationMode); + } + + return $hydrator; + } + + /** + * Gets the proxy factory used by the EntityManager to create entity proxies. + * + * @return ProxyFactory + */ + public function getProxyFactory() + { + return $this->proxyFactory; + } + + /** + * Factory method to create EntityManager instances. + * + * @param mixed $conn An array with the connection parameters or an existing + * Connection instance. + * @param Configuration $config The Configuration instance to use. + * @param EventManager $eventManager The EventManager instance to use. + * @return EntityManager The created EntityManager. + */ + public static function create($conn, Configuration $config, EventManager $eventManager = null) + { + if (!$config->getMetadataDriverImpl()) { + throw ORMException::missingMappingDriverImpl(); + } + + if (is_array($conn)) { + $conn = \Doctrine\DBAL\DriverManager::getConnection($conn, $config, ($eventManager ?: new EventManager())); + } else if ($conn instanceof Connection) { + if ($eventManager !== null && $conn->getEventManager() !== $eventManager) { + throw ORMException::mismatchedEventManager(); + } + } else { + throw new \InvalidArgumentException("Invalid argument: " . $conn); + } + + return new EntityManager($conn, $config, $conn->getEventManager()); + } +} diff --git a/Doctrine/ORM/EntityNotFoundException.php b/Doctrine/ORM/EntityNotFoundException.php new file mode 100644 index 0000000..2e58132 --- /dev/null +++ b/Doctrine/ORM/EntityNotFoundException.php @@ -0,0 +1,34 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Exception thrown when a Proxy fails to retrieve an Entity result. + * + * @author robo + * @since 2.0 + */ +class EntityNotFoundException extends ORMException +{ + public function __construct() + { + parent::__construct('Entity was not found.'); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/EntityRepository.php b/Doctrine/ORM/EntityRepository.php new file mode 100644 index 0000000..8168064 --- /dev/null +++ b/Doctrine/ORM/EntityRepository.php @@ -0,0 +1,225 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\DBAL\LockMode; + +/** + * An EntityRepository serves as a repository for entities with generic as well as + * business specific methods for retrieving entities. + * + * This class is designed for inheritance and users can subclass this class to + * write their own repositories with business-specific methods to locate entities. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EntityRepository +{ + /** + * @var string + */ + protected $_entityName; + + /** + * @var EntityManager + */ + protected $_em; + + /** + * @var Doctrine\ORM\Mapping\ClassMetadata + */ + protected $_class; + + /** + * Initializes a new EntityRepository. + * + * @param EntityManager $em The EntityManager to use. + * @param ClassMetadata $classMetadata The class descriptor. + */ + public function __construct($em, Mapping\ClassMetadata $class) + { + $this->_entityName = $class->name; + $this->_em = $em; + $this->_class = $class; + } + + /** + * Create a new QueryBuilder instance that is prepopulated for this entity name + * + * @param string $alias + * @return QueryBuilder $qb + */ + public function createQueryBuilder($alias) + { + return $this->_em->createQueryBuilder() + ->select($alias) + ->from($this->_entityName, $alias); + } + + /** + * Clears the repository, causing all managed entities to become detached. + */ + public function clear() + { + $this->_em->clear($this->_class->rootEntityName); + } + + /** + * Finds an entity by its primary key / identifier. + * + * @param $id The identifier. + * @param int $lockMode + * @param int $lockVersion + * @return object The entity. + */ + public function find($id, $lockMode = LockMode::NONE, $lockVersion = null) + { + // Check identity map first + if ($entity = $this->_em->getUnitOfWork()->tryGetById($id, $this->_class->rootEntityName)) { + if ($lockMode != LockMode::NONE) { + $this->_em->lock($entity, $lockMode, $lockVersion); + } + + return $entity; // Hit! + } + + if ( ! is_array($id) || count($id) <= 1) { + // @todo FIXME: Not correct. Relies on specific order. + $value = is_array($id) ? array_values($id) : array($id); + $id = array_combine($this->_class->identifier, $value); + } + + if ($lockMode == LockMode::NONE) { + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id); + } else if ($lockMode == LockMode::OPTIMISTIC) { + if (!$this->_class->isVersioned) { + throw OptimisticLockException::notVersioned($this->_entityName); + } + $entity = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id); + + $this->_em->getUnitOfWork()->lock($entity, $lockMode, $lockVersion); + + return $entity; + } else { + if (!$this->_em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($id, null, null, array(), $lockMode); + } + } + + /** + * Finds all entities in the repository. + * + * @return array The entities. + */ + public function findAll() + { + return $this->findBy(array()); + } + + /** + * Finds entities by a set of criteria. + * + * @param array $criteria + * @return array + */ + public function findBy(array $criteria) + { + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->loadAll($criteria); + } + + /** + * Finds a single entity by a set of criteria. + * + * @param array $criteria + * @return object + */ + public function findOneBy(array $criteria) + { + return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->load($criteria); + } + + /** + * Adds support for magic finders. + * + * @return array|object The found entity/entities. + * @throws BadMethodCallException If the method called is an invalid find* method + * or no find* method at all and therefore an invalid + * method call. + */ + public function __call($method, $arguments) + { + if (substr($method, 0, 6) == 'findBy') { + $by = substr($method, 6, strlen($method)); + $method = 'findBy'; + } else if (substr($method, 0, 9) == 'findOneBy') { + $by = substr($method, 9, strlen($method)); + $method = 'findOneBy'; + } else { + throw new \BadMethodCallException( + "Undefined method '$method'. The method name must start with ". + "either findBy or findOneBy!" + ); + } + + if ( !isset($arguments[0])) { + // we dont even want to allow null at this point, because we cannot (yet) transform it into IS NULL. + throw ORMException::findByRequiresParameter($method.$by); + } + + $fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by)); + + if ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName)) { + return $this->$method(array($fieldName => $arguments[0])); + } else { + throw ORMException::invalidFindByCall($this->_entityName, $fieldName, $method.$by); + } + } + + /** + * @return string + */ + protected function getEntityName() + { + return $this->_entityName; + } + + /** + * @return EntityManager + */ + protected function getEntityManager() + { + return $this->_em; + } + + /** + * @return Mapping\ClassMetadata + */ + protected function getClassMetadata() + { + return $this->_class; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Event/LifecycleEventArgs.php b/Doctrine/ORM/Event/LifecycleEventArgs.php new file mode 100644 index 0000000..a5dd39c --- /dev/null +++ b/Doctrine/ORM/Event/LifecycleEventArgs.php @@ -0,0 +1,60 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +/** + * Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions + * of entities. + * + * @since 2.0 + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class LifecycleEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var EntityManager + */ + private $_em; + + /** + * @var object + */ + private $_entity; + + public function __construct($entity, $em) + { + $this->_entity = $entity; + $this->_em = $em; + } + + public function getEntity() + { + return $this->_entity; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Event/LoadClassMetadataEventArgs.php b/Doctrine/ORM/Event/LoadClassMetadataEventArgs.php new file mode 100644 index 0000000..f00520a --- /dev/null +++ b/Doctrine/ORM/Event/LoadClassMetadataEventArgs.php @@ -0,0 +1,54 @@ + + * @since 2.0 + */ +class LoadClassMetadataEventArgs extends EventArgs +{ + /** + * @var ClassMetadata + */ + private $classMetadata; + + /** + * @var EntityManager + */ + private $em; + + /** + * @param ClassMetadataInfo $classMetadata + * @param EntityManager $em + */ + public function __construct(ClassMetadataInfo $classMetadata, EntityManager $em) + { + $this->classMetadata = $classMetadata; + $this->em = $em; + } + + /** + * @return ClassMetadataInfo + */ + public function getClassMetadata() + { + return $this->classMetadata; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->em; + } +} + diff --git a/Doctrine/ORM/Event/OnFlushEventArgs.php b/Doctrine/ORM/Event/OnFlushEventArgs.php new file mode 100644 index 0000000..1b4cb9b --- /dev/null +++ b/Doctrine/ORM/Event/OnFlushEventArgs.php @@ -0,0 +1,78 @@ +. +*/ + +namespace Doctrine\ORM\Event; + +/** + * Provides event arguments for the preFlush event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @version $Revision$ + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class OnFlushEventArgs extends \Doctrine\Common\EventArgs +{ + /** + * @var EntityManager + */ + private $_em; + + //private $_entitiesToPersist = array(); + //private $_entitiesToRemove = array(); + + public function __construct($em) + { + $this->_em = $em; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /* + public function addEntityToPersist($entity) + { + + } + + public function addEntityToRemove($entity) + { + + } + + public function addEntityToUpdate($entity) + { + + } + + public function getEntitiesToPersist() + { + return $this->_entitiesToPersist; + } + */ +} \ No newline at end of file diff --git a/Doctrine/ORM/Event/PreUpdateEventArgs.php b/Doctrine/ORM/Event/PreUpdateEventArgs.php new file mode 100644 index 0000000..c61e26d --- /dev/null +++ b/Doctrine/ORM/Event/PreUpdateEventArgs.php @@ -0,0 +1,98 @@ + + * @author Benjamin Eberlei + * @since 2.0 + */ +class PreUpdateEventArgs extends LifecycleEventArgs +{ + /** + * @var array + */ + private $_entityChangeSet; + + /** + * + * @param object $entity + * @param EntityManager $em + * @param array $changeSet + */ + public function __construct($entity, $em, array &$changeSet) + { + parent::__construct($entity, $em); + $this->_entityChangeSet = &$changeSet; + } + + public function getEntityChangeSet() + { + return $this->_entityChangeSet; + } + + /** + * Field has a changeset? + * + * @return bool + */ + public function hasChangedField($field) + { + return isset($this->_entityChangeSet[$field]); + } + + /** + * Get the old value of the changeset of the changed field. + * + * @param string $field + * @return mixed + */ + public function getOldValue($field) + { + $this->_assertValidField($field); + + return $this->_entityChangeSet[$field][0]; + } + + /** + * Get the new value of the changeset of the changed field. + * + * @param string $field + * @return mixed + */ + public function getNewValue($field) + { + $this->_assertValidField($field); + + return $this->_entityChangeSet[$field][1]; + } + + /** + * Set the new value of this field. + * + * @param string $field + * @param mixed $value + */ + public function setNewValue($field, $value) + { + $this->_assertValidField($field); + + $this->_entityChangeSet[$field][1] = $value; + } + + private function _assertValidField($field) + { + if (!isset($this->_entityChangeSet[$field])) { + throw new \InvalidArgumentException( + "Field '".$field."' is not a valid field of the entity ". + "'".get_class($this->getEntity())."' in PreInsertUpdateEventArgs." + ); + } + } +} + diff --git a/Doctrine/ORM/Events.php b/Doctrine/ORM/Events.php new file mode 100644 index 0000000..e25de65 --- /dev/null +++ b/Doctrine/ORM/Events.php @@ -0,0 +1,122 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Container for all ORM events. + * + * This class cannot be instantiated. + * + * @author Roman Borschel + * @since 2.0 + */ +final class Events +{ + private function __construct() {} + /** + * The preRemove event occurs for a given entity before the respective + * EntityManager remove operation for that entity is executed. + * + * This is an entity lifecycle event. + * + * @var string + */ + const preRemove = 'preRemove'; + /** + * The postRemove event occurs for an entity after the entity has + * been deleted. It will be invoked after the database delete operations. + * + * This is an entity lifecycle event. + * + * @var string + */ + const postRemove = 'postRemove'; + /** + * The prePersist event occurs for a given entity before the respective + * EntityManager persist operation for that entity is executed. + * + * This is an entity lifecycle event. + * + * @var string + */ + const prePersist = 'prePersist'; + /** + * The postPersist event occurs for an entity after the entity has + * been made persistent. It will be invoked after the database insert operations. + * Generated primary key values are available in the postPersist event. + * + * This is an entity lifecycle event. + * + * @var string + */ + const postPersist = 'postPersist'; + /** + * The preUpdate event occurs before the database update operations to + * entity data. + * + * This is an entity lifecycle event. + * + * @var string + */ + const preUpdate = 'preUpdate'; + /** + * The postUpdate event occurs after the database update operations to + * entity data. + * + * This is an entity lifecycle event. + * + * @var string + */ + const postUpdate = 'postUpdate'; + /** + * The postLoad event occurs for an entity after the entity has been loaded + * into the current EntityManager from the database or after the refresh operation + * has been applied to it. + * + * Note that the postLoad event occurs for an entity before any associations have been + * initialized. Therefore it is not safe to access associations in a postLoad callback + * or event handler. + * + * This is an entity lifecycle event. + * + * @var string + */ + const postLoad = 'postLoad'; + /** + * The loadClassMetadata event occurs after the mapping metadata for a class + * has been loaded from a mapping source (annotations/xml/yaml). + * + * @var string + */ + const loadClassMetadata = 'loadClassMetadata'; + + /** + * The onFlush event occurs when the EntityManager#flush() operation is invoked, + * after any changes to managed entities have been determined but before any + * actual database operations are executed. The event is only raised if there is + * actually something to do for the underlying UnitOfWork. If nothing needs to be done, + * the onFlush event is not raised. + * + * @var string + */ + const onFlush = 'onFlush'; +} \ No newline at end of file diff --git a/Doctrine/ORM/Id/AbstractIdGenerator.php b/Doctrine/ORM/Id/AbstractIdGenerator.php new file mode 100644 index 0000000..cfe3b5d --- /dev/null +++ b/Doctrine/ORM/Id/AbstractIdGenerator.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Id; + +use Doctrine\ORM\EntityManager; + +abstract class AbstractIdGenerator +{ + /** + * Generates an identifier for an entity. + * + * @param Doctrine\ORM\Entity $entity + * @return mixed + */ + abstract public function generate(EntityManager $em, $entity); + + /** + * Gets whether this generator is a post-insert generator which means that + * {@link generate()} must be called after the entity has been inserted + * into the database. + * + * By default, this method returns FALSE. Generators that have this requirement + * must override this method and return TRUE. + * + * @return boolean + */ + public function isPostInsertGenerator() + { + return false; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Id/AssignedGenerator.php b/Doctrine/ORM/Id/AssignedGenerator.php new file mode 100644 index 0000000..f4bd3d6 --- /dev/null +++ b/Doctrine/ORM/Id/AssignedGenerator.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\ORM\Id; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\ORMException; + +/** + * Special generator for application-assigned identifiers (doesnt really generate anything). + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AssignedGenerator extends AbstractIdGenerator +{ + /** + * Returns the identifier assigned to the given entity. + * + * @param object $entity + * @return mixed + * @override + */ + public function generate(EntityManager $em, $entity) + { + $class = $em->getClassMetadata(get_class($entity)); + $identifier = array(); + if ($class->isIdentifierComposite) { + $idFields = $class->getIdentifierFieldNames(); + foreach ($idFields as $idField) { + $value = $class->getReflectionProperty($idField)->getValue($entity); + if (isset($value)) { + $identifier[$idField] = $value; + } else { + throw ORMException::entityMissingAssignedId($entity); + } + } + } else { + $idField = $class->identifier[0]; + $value = $class->reflFields[$idField]->getValue($entity); + if (isset($value)) { + $identifier[$idField] = $value; + } else { + throw ORMException::entityMissingAssignedId($entity); + } + } + + return $identifier; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Id/IdentityGenerator.php b/Doctrine/ORM/Id/IdentityGenerator.php new file mode 100644 index 0000000..75da273 --- /dev/null +++ b/Doctrine/ORM/Id/IdentityGenerator.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Id; + +use Doctrine\ORM\EntityManager; + +/** + * Id generator that obtains IDs from special "identity" columns. These are columns + * that automatically get a database-generated, auto-incremented identifier on INSERT. + * This generator obtains the last insert id after such an insert. + */ +class IdentityGenerator extends AbstractIdGenerator +{ + /** @var string The name of the sequence to pass to lastInsertId(), if any. */ + private $_seqName; + + /** + * @param string $seqName The name of the sequence to pass to lastInsertId() + * to obtain the last generated identifier within the current + * database session/connection, if any. + */ + public function __construct($seqName = null) + { + $this->_seqName = $seqName; + } + + /** + * {@inheritdoc} + */ + public function generate(EntityManager $em, $entity) + { + return $em->getConnection()->lastInsertId($this->_seqName); + } + + /** + * {@inheritdoc} + */ + public function isPostInsertGenerator() + { + return true; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Id/SequenceGenerator.php b/Doctrine/ORM/Id/SequenceGenerator.php new file mode 100644 index 0000000..0d564ed --- /dev/null +++ b/Doctrine/ORM/Id/SequenceGenerator.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\ORM\Id; + +use Serializable, Doctrine\ORM\EntityManager; + +/** + * Represents an ID generator that uses a database sequence. + * + * @since 2.0 + * @author Roman Borschel + */ +class SequenceGenerator extends AbstractIdGenerator implements Serializable +{ + private $_allocationSize; + private $_sequenceName; + private $_nextValue = 0; + private $_maxValue = null; + + /** + * Initializes a new sequence generator. + * + * @param Doctrine\ORM\EntityManager $em The EntityManager to use. + * @param string $sequenceName The name of the sequence. + * @param integer $allocationSize The allocation size of the sequence. + */ + public function __construct($sequenceName, $allocationSize) + { + $this->_sequenceName = $sequenceName; + $this->_allocationSize = $allocationSize; + } + + /** + * Generates an ID for the given entity. + * + * @param object $entity + * @return integer|float The generated value. + * @override + */ + public function generate(EntityManager $em, $entity) + { + if ($this->_maxValue === null || $this->_nextValue == $this->_maxValue) { + // Allocate new values + $conn = $em->getConnection(); + $sql = $conn->getDatabasePlatform()->getSequenceNextValSQL($this->_sequenceName); + $this->_nextValue = $conn->fetchColumn($sql); + $this->_maxValue = $this->_nextValue + $this->_allocationSize; + } + return $this->_nextValue++; + } + + /** + * Gets the maximum value of the currently allocated bag of values. + * + * @return integer|float + */ + public function getCurrentMaxValue() + { + return $this->_maxValue; + } + + /** + * Gets the next value that will be returned by generate(). + * + * @return integer|float + */ + public function getNextValue() + { + return $this->_nextValue; + } + + public function serialize() + { + return serialize(array( + 'allocationSize' => $this->_allocationSize, + 'sequenceName' => $this->_sequenceName + )); + } + + public function unserialize($serialized) + { + $array = unserialize($serialized); + $this->_sequenceName = $array['sequenceName']; + $this->_allocationSize = $array['allocationSize']; + } +} diff --git a/Doctrine/ORM/Id/TableGenerator.php b/Doctrine/ORM/Id/TableGenerator.php new file mode 100644 index 0000000..5c46f8b --- /dev/null +++ b/Doctrine/ORM/Id/TableGenerator.php @@ -0,0 +1,79 @@ +. + */ + +namespace Doctrine\ORM\Id; + +use Doctrine\ORM\EntityManager; + +/** + * Id generator that uses a single-row database table and a hi/lo algorithm. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class TableGenerator extends AbstractIdGenerator +{ + private $_tableName; + private $_sequenceName; + private $_allocationSize; + private $_nextValue; + private $_maxValue; + + public function __construct($tableName, $sequenceName = 'default', $allocationSize = 10) + { + $this->_tableName = $tableName; + $this->_sequenceName = $sequenceName; + $this->_allocationSize = $allocationSize; + } + + public function generate(EntityManager $em, $entity) + { + if ($this->_maxValue === null || $this->_nextValue == $this->_maxValue) { + // Allocate new values + $conn = $em->getConnection(); + if ($conn->getTransactionNestingLevel() == 0) { + + // use select for update + $sql = $conn->getDatabasePlatform()->getTableHiLoCurrentValSql($this->_tableName, $this->_sequenceName); + $currentLevel = $conn->fetchColumn($sql); + if ($currentLevel != null) { + $this->_nextValue = $currentLevel; + $this->_maxValue = $this->_nextValue + $this->_allocationSize; + + $updateSql = $conn->getDatabasePlatform()->getTableHiLoUpdateNextValSql( + $this->_tableName, $this->_sequenceName, $this->_allocationSize + ); + + if ($conn->executeUpdate($updateSql, array(1 => $currentLevel, 2 => $currentLevel+1)) !== 1) { + // no affected rows, concurrency issue, throw exception + } + } else { + // no current level returned, TableGenerator seems to be broken, throw exception + } + } else { + // only table locks help here, implement this or throw exception? + // or do we want to work with table locks exclusively? + } + } + return $this->_nextValue++; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Internal/CommitOrderCalculator.php b/Doctrine/ORM/Internal/CommitOrderCalculator.php new file mode 100644 index 0000000..8997b1e --- /dev/null +++ b/Doctrine/ORM/Internal/CommitOrderCalculator.php @@ -0,0 +1,118 @@ +. + */ + +namespace Doctrine\ORM\Internal; + +/** + * The CommitOrderCalculator is used by the UnitOfWork to sort out the + * correct order in which changes to entities need to be persisted. + * + * @since 2.0 + * @author Roman Borschel + */ +class CommitOrderCalculator +{ + const NOT_VISITED = 1; + const IN_PROGRESS = 2; + const VISITED = 3; + + private $_nodeStates = array(); + private $_classes = array(); // The nodes to sort + private $_relatedClasses = array(); + private $_sorted = array(); + + /** + * Clears the current graph. + * + * @return void + */ + public function clear() + { + $this->_classes = + $this->_relatedClasses = array(); + } + + /** + * Gets a valid commit order for all current nodes. + * + * Uses a depth-first search (DFS) to traverse the graph. + * The desired topological sorting is the reverse postorder of these searches. + * + * @return array The list of ordered classes. + */ + public function getCommitOrder() + { + // Check whether we need to do anything. 0 or 1 node is easy. + $nodeCount = count($this->_classes); + if ($nodeCount == 0) { + return array(); + } else if ($nodeCount == 1) { + return array_values($this->_classes); + } + + // Init + foreach ($this->_classes as $node) { + $this->_nodeStates[$node->name] = self::NOT_VISITED; + } + + // Go + foreach ($this->_classes as $node) { + if ($this->_nodeStates[$node->name] == self::NOT_VISITED) { + $this->_visitNode($node); + } + } + + $sorted = array_reverse($this->_sorted); + + $this->_sorted = $this->_nodeStates = array(); + + return $sorted; + } + + private function _visitNode($node) + { + $this->_nodeStates[$node->name] = self::IN_PROGRESS; + + if (isset($this->_relatedClasses[$node->name])) { + foreach ($this->_relatedClasses[$node->name] as $relatedNode) { + if ($this->_nodeStates[$relatedNode->name] == self::NOT_VISITED) { + $this->_visitNode($relatedNode); + } + } + } + + $this->_nodeStates[$node->name] = self::VISITED; + $this->_sorted[] = $node; + } + + public function addDependency($fromClass, $toClass) + { + $this->_relatedClasses[$fromClass->name][] = $toClass; + } + + public function hasClass($className) + { + return isset($this->_classes[$className]); + } + + public function addClass($class) + { + $this->_classes[$class->name] = $class; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php new file mode 100644 index 0000000..5ce4621 --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -0,0 +1,278 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +use PDO, + Doctrine\DBAL\Connection, + Doctrine\DBAL\Types\Type, + Doctrine\ORM\EntityManager; + +/** + * Base class for all hydrators. A hydrator is a class that provides some form + * of transformation of an SQL result set into another structure. + * + * @since 2.0 + * @author Konsta Vesterinen + * @author Roman Borschel + */ +abstract class AbstractHydrator +{ + /** @var ResultSetMapping The ResultSetMapping. */ + protected $_rsm; + + /** @var EntityManager The EntityManager instance. */ + protected $_em; + + /** @var AbstractPlatform The dbms Platform instance */ + protected $_platform; + + /** @var UnitOfWork The UnitOfWork of the associated EntityManager. */ + protected $_uow; + + /** @var array The cache used during row-by-row hydration. */ + protected $_cache = array(); + + /** @var Statement The statement that provides the data to hydrate. */ + protected $_stmt; + + /** @var array The query hints. */ + protected $_hints; + + /** + * Initializes a new instance of a class derived from AbstractHydrator. + * + * @param Doctrine\ORM\EntityManager $em The EntityManager to use. + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + $this->_platform = $em->getConnection()->getDatabasePlatform(); + $this->_uow = $em->getUnitOfWork(); + } + + /** + * Initiates a row-by-row hydration. + * + * @param object $stmt + * @param object $resultSetMapping + * @return IterableResult + */ + public function iterate($stmt, $resultSetMapping, array $hints = array()) + { + $this->_stmt = $stmt; + $this->_rsm = $resultSetMapping; + $this->_hints = $hints; + $this->_prepare(); + return new IterableResult($this); + } + + /** + * Hydrates all rows returned by the passed statement instance at once. + * + * @param object $stmt + * @param object $resultSetMapping + * @return mixed + */ + public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) + { + $this->_stmt = $stmt; + $this->_rsm = $resultSetMapping; + $this->_hints = $hints; + $this->_prepare(); + $result = $this->_hydrateAll(); + $this->_cleanup(); + return $result; + } + + /** + * Hydrates a single row returned by the current statement instance during + * row-by-row hydration with {@link iterate()}. + * + * @return mixed + */ + public function hydrateRow() + { + $row = $this->_stmt->fetch(PDO::FETCH_ASSOC); + if ( ! $row) { + $this->_cleanup(); + return false; + } + $result = array(); + $this->_hydrateRow($row, $this->_cache, $result); + return $result; + } + + /** + * Excutes one-time preparation tasks, once each time hydration is started + * through {@link hydrateAll} or {@link iterate()}. + */ + protected function _prepare() + {} + + /** + * Excutes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link iterate()}. + */ + protected function _cleanup() + { + $this->_rsm = null; + $this->_stmt->closeCursor(); + $this->_stmt = null; + } + + /** + * Hydrates a single row from the current statement instance. + * + * Template method. + * + * @param array $data The row data. + * @param array $cache The cache to use. + * @param mixed $result The result to fill. + */ + protected function _hydrateRow(array $data, array &$cache, array &$result) + { + throw new HydrationException("_hydrateRow() not implemented by this hydrator."); + } + + /** + * Hydrates all rows from the current statement instance at once. + */ + abstract protected function _hydrateAll(); + + /** + * Processes a row of the result set. + * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). + * Puts the elements of a result row into a new array, grouped by the class + * they belong to. The column names in the result set are mapped to their + * field names during this procedure as well as any necessary conversions on + * the values applied. + * + * @return array An array with all the fields (name => value) of the data row, + * grouped by their component alias. + */ + protected function _gatherRowData(array $data, array &$cache, array &$id, array &$nonemptyComponents) + { + $rowData = array(); + + foreach ($data as $key => $value) { + // Parse each column name only once. Cache the results. + if ( ! isset($cache[$key])) { + if (isset($this->_rsm->scalarMappings[$key])) { + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + } else if (isset($this->_rsm->fieldMappings[$key])) { + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + } else if (!isset($this->_rsm->metaMappings[$key])) { + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue; + } else { + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + } + } + + if (isset($cache[$key]['isScalar'])) { + $rowData['scalars'][$cache[$key]['fieldName']] = $value; + continue; + } + + $dqlAlias = $cache[$key]['dqlAlias']; + + if (isset($cache[$key]['isMetaColumn'])) { + $rowData[$dqlAlias][$cache[$key]['fieldName']] = $value; + continue; + } + + if ($cache[$key]['isIdentifier']) { + $id[$dqlAlias] .= '|' . $value; + } + + $rowData[$dqlAlias][$cache[$key]['fieldName']] = $cache[$key]['type']->convertToPHPValue($value, $this->_platform); + + if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) { + $nonemptyComponents[$dqlAlias] = true; + } + } + + return $rowData; + } + + /** + * Processes a row of the result set. + * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that + * simply converts column names to field names and properly converts the + * values according to their types. The resulting row has the same number + * of elements as before. + * + * @param array $data + * @param array $cache + * @return array The processed row. + */ + protected function _gatherScalarRowData(&$data, &$cache) + { + $rowData = array(); + + foreach ($data as $key => $value) { + // Parse each column name only once. Cache the results. + if ( ! isset($cache[$key])) { + if (isset($this->_rsm->scalarMappings[$key])) { + $cache[$key]['fieldName'] = $this->_rsm->scalarMappings[$key]; + $cache[$key]['isScalar'] = true; + } else if (isset($this->_rsm->fieldMappings[$key])) { + $fieldName = $this->_rsm->fieldMappings[$key]; + $classMetadata = $this->_em->getClassMetadata($this->_rsm->declaringClasses[$key]); + $cache[$key]['fieldName'] = $fieldName; + $cache[$key]['type'] = Type::getType($classMetadata->fieldMappings[$fieldName]['type']); + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + } else if (!isset($this->_rsm->metaMappings[$key])) { + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + continue; + } else { + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $cache[$key]['isMetaColumn'] = true; + $cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key]; + $cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key]; + } + } + + $fieldName = $cache[$key]['fieldName']; + + if (isset($cache[$key]['isScalar'])) { + $rowData[$fieldName] = $value; + } else if (isset($cache[$key]['isMetaColumn'])) { + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $value; + } else { + $rowData[$cache[$key]['dqlAlias'] . '_' . $fieldName] = $cache[$key]['type'] + ->convertToPHPValue($value, $this->_platform); + } + } + + return $rowData; + } +} diff --git a/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php new file mode 100644 index 0000000..92eb45c --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -0,0 +1,235 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +use PDO, Doctrine\DBAL\Connection, Doctrine\ORM\Mapping\ClassMetadata; + +/** + * The ArrayHydrator produces a nested array "graph" that is often (not always) + * interchangeable with the corresponding object graph for read-only access. + * + * @author Roman Borschel + * @since 1.0 + */ +class ArrayHydrator extends AbstractHydrator +{ + private $_ce = array(); + private $_rootAliases = array(); + private $_isSimpleQuery = false; + private $_identifierMap = array(); + private $_resultPointers = array(); + private $_idTemplate = array(); + private $_resultCounter = 0; + + /** @override */ + protected function _prepare() + { + $this->_isSimpleQuery = count($this->_rsm->aliasMap) <= 1; + $this->_identifierMap = array(); + $this->_resultPointers = array(); + $this->_idTemplate = array(); + $this->_resultCounter = 0; + foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { + $this->_identifierMap[$dqlAlias] = array(); + $this->_resultPointers[$dqlAlias] = array(); + $this->_idTemplate[$dqlAlias] = ''; + } + } + + /** @override */ + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($data, $cache, $result); + } + + return $result; + } + + /** @override */ + protected function _hydrateRow(array $data, array &$cache, array &$result) + { + // 1) Initialize + $id = $this->_idTemplate; // initialize the id-memory + $nonemptyComponents = array(); + $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); + + // Extract scalar values. They're appended at the end. + if (isset($rowData['scalars'])) { + $scalars = $rowData['scalars']; + unset($rowData['scalars']); + if (empty($rowData)) { + ++$this->_resultCounter; + } + } + + // 2) Now hydrate the data found in the current row. + foreach ($rowData as $dqlAlias => $data) { + $index = false; + + if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parent = $this->_rsm->parentAliasMap[$dqlAlias]; + $path = $parent . '.' . $dqlAlias; + + // Get a reference to the right element in the result tree. + // This element will get the associated element attached. + if ($this->_rsm->isMixed && isset($this->_rootAliases[$parent])) { + $first = reset($this->_resultPointers); + // TODO: Exception if $key === null ? + $baseElement =& $this->_resultPointers[$parent][key($first)]; + } else if (isset($this->_resultPointers[$parent])) { + $baseElement =& $this->_resultPointers[$parent]; + } else { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + continue; + } + + $relationAlias = $this->_rsm->relationMap[$dqlAlias]; + $relation = $this->_getClassMetadata($this->_rsm->aliasMap[$parent])->associationMappings[$relationAlias]; + + // Check the type of the relation (many or single-valued) + if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { + $oneToOne = false; + if (isset($nonemptyComponents[$dqlAlias])) { + if ( ! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = array(); + } + + $indexExists = isset($this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; + + if ( ! $indexExists || ! $indexIsValid) { + $element = $data; + if (isset($this->_rsm->indexByMap[$dqlAlias])) { + $field = $this->_rsm->indexByMap[$dqlAlias]; + $baseElement[$relationAlias][$element[$field]] = $element; + } else { + $baseElement[$relationAlias][] = $element; + } + end($baseElement[$relationAlias]); + $this->_identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = + key($baseElement[$relationAlias]); + } + } else if ( ! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = array(); + } + } else { + $oneToOne = true; + if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = null; + } else if ( ! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = $data; + } + } + + $coll =& $baseElement[$relationAlias]; + + if ($coll !== null) { + $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); + } + + } else { + // It's a root result element + + $this->_rootAliases[$dqlAlias] = true; // Mark as root + + // Check for an existing element + if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $rowData[$dqlAlias]; + if (isset($this->_rsm->indexByMap[$dqlAlias])) { + $field = $this->_rsm->indexByMap[$dqlAlias]; + if ($this->_rsm->isMixed) { + $result[] = array($element[$field] => $element); + ++$this->_resultCounter; + } else { + $result[$element[$field]] = $element; + } + } else { + if ($this->_rsm->isMixed) { + $result[] = array($element); + ++$this->_resultCounter; + } else { + $result[] = $element; + } + } + end($result); + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = key($result); + } else { + $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; + /*if ($this->_rsm->isMixed) { + $result[] =& $result[$index]; + ++$this->_resultCounter; + }*/ + } + $this->updateResultPointer($result, $index, $dqlAlias, false); + } + } + + // Append scalar values to mixed result sets + if (isset($scalars)) { + foreach ($scalars as $name => $value) { + $result[$this->_resultCounter - 1][$name] = $value; + } + } + } + + /** + * Updates the result pointer for an Entity. The result pointers point to the + * last seen instance of each Entity type. This is used for graph construction. + * + * @param array $coll The element. + * @param boolean|integer $index Index of the element in the collection. + * @param string $dqlAlias + * @param boolean $oneToOne Whether it is a single-valued association or not. + */ + private function updateResultPointer(array &$coll, $index, $dqlAlias, $oneToOne) + { + if ($coll === null) { + unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + return; + } + if ($index !== false) { + $this->_resultPointers[$dqlAlias] =& $coll[$index]; + return; + } else { + if ($coll) { + if ($oneToOne) { + $this->_resultPointers[$dqlAlias] =& $coll; + } else { + end($coll); + $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; + } + } + } + } + + private function _getClassMetadata($className) + { + if ( ! isset($this->_ce[$className])) { + $this->_ce[$className] = $this->_em->getClassMetadata($className); + } + return $this->_ce[$className]; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Internal/Hydration/HydrationException.php b/Doctrine/ORM/Internal/Hydration/HydrationException.php new file mode 100644 index 0000000..886b42d --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/HydrationException.php @@ -0,0 +1,17 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +/** + * Represents a result structure that can be iterated over, hydrating row-by-row + * during the iteration. An IterableResult is obtained by AbstractHydrator#iterate(). + * + * @author robo + * @since 2.0 + */ +class IterableResult implements \Iterator +{ + /** + * @var Doctrine\ORM\Internal\Hydration\AbstractHydrator + */ + private $_hydrator; + + /** + * @var boolean + */ + private $_rewinded = false; + + /** + * @var integer + */ + private $_key = -1; + + /** + * @var object + */ + private $_current = null; + + /** + * @param Doctrine\ORM\Internal\Hydration\AbstractHydrator $hydrator + */ + public function __construct($hydrator) + { + $this->_hydrator = $hydrator; + } + + public function rewind() + { + if ($this->_rewinded == true) { + throw new HydrationException("Can only iterate a Result once."); + } else { + $this->_current = $this->next(); + $this->_rewinded = true; + } + } + + /** + * Gets the next set of results. + * + * @return array + */ + public function next() + { + $this->_current = $this->_hydrator->hydrateRow(); + $this->_key++; + return $this->_current; + } + + /** + * @return mixed + */ + public function current() + { + return $this->_current; + } + + /** + * @return int + */ + public function key() + { + return $this->_key; + } + + /** + * @return bool + */ + public function valid() + { + return ($this->_current!=false); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php new file mode 100644 index 0000000..202fdc7 --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -0,0 +1,429 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +use PDO, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\PersistentCollection, + Doctrine\ORM\Query, + Doctrine\Common\Collections\ArrayCollection, + Doctrine\Common\Collections\Collection; + +/** + * The ObjectHydrator constructs an object graph out of an SQL result set. + * + * @author Roman Borschel + * @since 2.0 + * @internal Highly performance-sensitive code. + */ +class ObjectHydrator extends AbstractHydrator +{ + /* Local ClassMetadata cache to avoid going to the EntityManager all the time. + * This local cache is maintained between hydration runs and not cleared. + */ + private $_ce = array(); + + /* The following parts are reinitialized on every hydration run. */ + + private $_identifierMap; + private $_resultPointers; + private $_idTemplate; + private $_resultCounter; + private $_rootAliases = array(); + private $_initializedCollections = array(); + private $_existingCollections = array(); + //private $_createdEntities; + + + /** @override */ + protected function _prepare() + { + $this->_identifierMap = + $this->_resultPointers = + $this->_idTemplate = array(); + $this->_resultCounter = 0; + + foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { + $this->_identifierMap[$dqlAlias] = array(); + $this->_idTemplate[$dqlAlias] = ''; + $class = $this->_em->getClassMetadata($className); + + if ( ! isset($this->_ce[$className])) { + $this->_ce[$className] = $class; + } + + // Remember which associations are "fetch joined", so that we know where to inject + // collection stubs or proxies and where not. + if (isset($this->_rsm->relationMap[$dqlAlias])) { + $sourceClassName = $this->_rsm->aliasMap[$this->_rsm->parentAliasMap[$dqlAlias]]; + $sourceClass = $this->_getClassMetadata($sourceClassName); + $assoc = $sourceClass->associationMappings[$this->_rsm->relationMap[$dqlAlias]]; + $this->_hints['fetched'][$sourceClassName][$assoc['fieldName']] = true; + if ($sourceClass->subClasses) { + foreach ($sourceClass->subClasses as $sourceSubclassName) { + $this->_hints['fetched'][$sourceSubclassName][$assoc['fieldName']] = true; + } + } + if ($assoc['type'] != ClassMetadata::MANY_TO_MANY) { + // Mark any non-collection opposite sides as fetched, too. + if ($assoc['mappedBy']) { + $this->_hints['fetched'][$className][$assoc['mappedBy']] = true; + } else { + if ($assoc['inversedBy']) { + $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; + if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { + $this->_hints['fetched'][$className][$inverseAssoc['fieldName']] = true; + if ($class->subClasses) { + foreach ($class->subClasses as $targetSubclassName) { + $this->_hints['fetched'][$targetSubclassName][$inverseAssoc['fieldName']] = true; + } + } + } + } + } + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function _cleanup() + { + parent::_cleanup(); + $this->_identifierMap = + $this->_initializedCollections = + $this->_existingCollections = + $this->_resultPointers = array(); + } + + /** + * {@inheritdoc} + */ + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + + while ($row = $this->_stmt->fetch(PDO::FETCH_ASSOC)) { + $this->_hydrateRow($row, $cache, $result); + } + + // Take snapshots from all newly initialized collections + foreach ($this->_initializedCollections as $coll) { + $coll->takeSnapshot(); + } + + return $result; + } + + /** + * Initializes a related collection. + * + * @param object $entity The entity to which the collection belongs. + * @param string $name The name of the field on the entity that holds the collection. + */ + private function _initRelatedCollection($entity, $class, $fieldName) + { + $oid = spl_object_hash($entity); + $relation = $class->associationMappings[$fieldName]; + + $value = $class->reflFields[$fieldName]->getValue($entity); + if ($value === null) { + $value = new ArrayCollection; + } + + if ( ! $value instanceof PersistentCollection) { + $value = new PersistentCollection( + $this->_em, + $this->_ce[$relation['targetEntity']], + $value + ); + $value->setOwner($entity, $relation); + $class->reflFields[$fieldName]->setValue($entity, $value); + $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); + $this->_initializedCollections[$oid . $fieldName] = $value; + } else if (isset($this->_hints[Query::HINT_REFRESH]) || + isset($this->_hints['fetched'][$class->name][$fieldName]) && + ! $value->isInitialized()) { + // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! + $value->setDirty(false); + $value->setInitialized(true); + $value->unwrap()->clear(); + $this->_initializedCollections[$oid . $fieldName] = $value; + } else { + // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! + $this->_existingCollections[$oid . $fieldName] = $value; + } + + return $value; + } + + /** + * Gets an entity instance. + * + * @param $data The instance data. + * @param $dqlAlias The DQL alias of the entity's class. + * @return object The entity. + */ + private function _getEntity(array $data, $dqlAlias) + { + $className = $this->_rsm->aliasMap[$dqlAlias]; + if (isset($this->_rsm->discriminatorColumns[$dqlAlias])) { + $discrColumn = $this->_rsm->metaMappings[$this->_rsm->discriminatorColumns[$dqlAlias]]; + $className = $this->_ce[$className]->discriminatorMap[$data[$discrColumn]]; + unset($data[$discrColumn]); + } + return $this->_uow->createEntity($className, $data, $this->_hints); + } + + private function _getEntityFromIdentityMap($className, array $data) + { + $class = $this->_ce[$className]; + if ($class->isIdentifierComposite) { + $idHash = ''; + foreach ($class->identifier as $fieldName) { + $idHash .= $data[$fieldName] . ' '; + } + return $this->_uow->tryGetByIdHash(rtrim($idHash), $class->rootEntityName); + } else { + return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); + } + } + + /** + * Gets a ClassMetadata instance from the local cache. + * If the instance is not yet in the local cache, it is loaded into the + * local cache. + * + * @param string $className The name of the class. + * @return ClassMetadata + */ + private function _getClassMetadata($className) + { + if ( ! isset($this->_ce[$className])) { + $this->_ce[$className] = $this->_em->getClassMetadata($className); + } + return $this->_ce[$className]; + } + + /** + * Hydrates a single row in an SQL result set. + * + * @internal + * First, the data of the row is split into chunks where each chunk contains data + * that belongs to a particular component/class. Afterwards, all these chunks + * are processed, one after the other. For each chunk of class data only one of the + * following code paths is executed: + * + * Path A: The data chunk belongs to a joined/associated object and the association + * is collection-valued. + * Path B: The data chunk belongs to a joined/associated object and the association + * is single-valued. + * Path C: The data chunk belongs to a root result element/object that appears in the topmost + * level of the hydrated result. A typical example are the objects of the type + * specified by the FROM clause in a DQL query. + * + * @param array $data The data of the row to process. + * @param array $cache The cache to use. + * @param array $result The result array to fill. + */ + protected function _hydrateRow(array $data, array &$cache, array &$result) + { + // Initialize + $id = $this->_idTemplate; // initialize the id-memory + $nonemptyComponents = array(); + // Split the row data into chunks of class data. + $rowData = $this->_gatherRowData($data, $cache, $id, $nonemptyComponents); + + // Extract scalar values. They're appended at the end. + if (isset($rowData['scalars'])) { + $scalars = $rowData['scalars']; + unset($rowData['scalars']); + if (empty($rowData)) { + ++$this->_resultCounter; + } + } + + // Hydrate the data chunks + foreach ($rowData as $dqlAlias => $data) { + $entityName = $this->_rsm->aliasMap[$dqlAlias]; + + if (isset($this->_rsm->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parentAlias = $this->_rsm->parentAliasMap[$dqlAlias]; + // we need the $path to save into the identifier map which entities were already + // seen for this parent-child relationship + $path = $parentAlias . '.' . $dqlAlias; + + // Get a reference to the parent object to which the joined element belongs. + if ($this->_rsm->isMixed && isset($this->_rootAliases[$parentAlias])) { + $first = reset($this->_resultPointers); + $parentObject = $this->_resultPointers[$parentAlias][key($first)]; + } else if (isset($this->_resultPointers[$parentAlias])) { + $parentObject = $this->_resultPointers[$parentAlias]; + } else { + // Parent object of relation not found, so skip it. + continue; + } + + $parentClass = $this->_ce[$this->_rsm->aliasMap[$parentAlias]]; + $oid = spl_object_hash($parentObject); + $relationField = $this->_rsm->relationMap[$dqlAlias]; + $relation = $parentClass->associationMappings[$relationField]; + $reflField = $parentClass->reflFields[$relationField]; + + // Check the type of the relation (many or single-valued) + if ( ! ($relation['type'] & ClassMetadata::TO_ONE)) { + // PATH A: Collection-valued association + if (isset($nonemptyComponents[$dqlAlias])) { + $collKey = $oid . $relationField; + if (isset($this->_initializedCollections[$collKey])) { + $reflFieldValue = $this->_initializedCollections[$collKey]; + } else if ( ! isset($this->_existingCollections[$collKey])) { + $reflFieldValue = $this->_initRelatedCollection($parentObject, $parentClass, $relationField); + } + + $indexExists = isset($this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); + $index = $indexExists ? $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; + + if ( ! $indexExists || ! $indexIsValid) { + if (isset($this->_existingCollections[$collKey])) { + // Collection exists, only look for the element in the identity map. + if ($element = $this->_getEntityFromIdentityMap($entityName, $data)) { + $this->_resultPointers[$dqlAlias] = $element; + } else { + unset($this->_resultPointers[$dqlAlias]); + } + } else { + $element = $this->_getEntity($data, $dqlAlias); + + if (isset($this->_rsm->indexByMap[$dqlAlias])) { + $field = $this->_rsm->indexByMap[$dqlAlias]; + $indexValue = $this->_ce[$entityName]->reflFields[$field]->getValue($element); + $reflFieldValue->hydrateSet($indexValue, $element); + $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; + } else { + $reflFieldValue->hydrateAdd($element); + $reflFieldValue->last(); + $this->_identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); + } + // Update result pointer + $this->_resultPointers[$dqlAlias] = $element; + } + } else { + // Update result pointer + $this->_resultPointers[$dqlAlias] = $reflFieldValue[$index]; + } + } else if ( ! $reflField->getValue($parentObject)) { + $coll = new PersistentCollection($this->_em, $this->_ce[$entityName], new ArrayCollection); + $coll->setOwner($parentObject, $relation); + $reflField->setValue($parentObject, $coll); + $this->_uow->setOriginalEntityProperty($oid, $relationField, $coll); + } + } else { + // PATH B: Single-valued association + $reflFieldValue = $reflField->getValue($parentObject); + if ( ! $reflFieldValue || isset($this->_hints[Query::HINT_REFRESH])) { + if (isset($nonemptyComponents[$dqlAlias])) { + $element = $this->_getEntity($data, $dqlAlias); + $reflField->setValue($parentObject, $element); + $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); + $targetClass = $this->_ce[$relation['targetEntity']]; + if ($relation['isOwningSide']) { + //TODO: Just check hints['fetched'] here? + // If there is an inverse mapping on the target class its bidirectional + if ($relation['inversedBy']) { + $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']]; + if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { + $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); + $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $inverseAssoc['fieldName'], $parentObject); + } + } else if ($parentClass === $targetClass && $relation['mappedBy']) { + // Special case: bi-directional self-referencing one-one on the same class + $targetClass->reflFields[$relationField]->setValue($element, $parentObject); + } + } else { + // For sure bidirectional, as there is no inverse side in unidirectional mappings + $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject); + $this->_uow->setOriginalEntityProperty(spl_object_hash($element), $relation['mappedBy'], $parentObject); + } + // Update result pointer + $this->_resultPointers[$dqlAlias] = $element; + } + // else leave $reflFieldValue null for single-valued associations + } else { + // Update result pointer + $this->_resultPointers[$dqlAlias] = $reflFieldValue; + } + } + } else { + // PATH C: Its a root result element + $this->_rootAliases[$dqlAlias] = true; // Mark as root alias + + if ( ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $this->_getEntity($rowData[$dqlAlias], $dqlAlias); + if (isset($this->_rsm->indexByMap[$dqlAlias])) { + $field = $this->_rsm->indexByMap[$dqlAlias]; + $key = $this->_ce[$entityName]->reflFields[$field]->getValue($element); + if ($this->_rsm->isMixed) { + $element = array($key => $element); + $result[] = $element; + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; + ++$this->_resultCounter; + } else { + $result[$key] = $element; + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $key; + } + } else { + if ($this->_rsm->isMixed) { + $element = array(0 => $element); + } + $result[] = $element; + $this->_identifierMap[$dqlAlias][$id[$dqlAlias]] = $this->_resultCounter; + ++$this->_resultCounter; + } + + // Update result pointer + $this->_resultPointers[$dqlAlias] = $element; + + } else { + // Update result pointer + $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; + $this->_resultPointers[$dqlAlias] = $result[$index]; + /*if ($this->_rsm->isMixed) { + $result[] = $result[$index]; + ++$this->_resultCounter; + }*/ + } + } + } + + // Append scalar values to mixed result sets + if (isset($scalars)) { + foreach ($scalars as $name => $value) { + $result[$this->_resultCounter - 1][$name] = $value; + } + } + } +} diff --git a/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php b/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php new file mode 100644 index 0000000..f153073 --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/ScalarHydrator.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +use Doctrine\DBAL\Connection; + +/** + * Hydrator that produces flat, rectangular results of scalar data. + * The created result is almost the same as a regular SQL result set, except + * that column names are mapped to field names and data type conversions take place. + * + * @author Roman Borschel + * @since 2.0 + */ +class ScalarHydrator extends AbstractHydrator +{ + /** @override */ + protected function _hydrateAll() + { + $result = array(); + $cache = array(); + while ($data = $this->_stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $this->_gatherScalarRowData($data, $cache); + } + return $result; + } + + /** @override */ + protected function _hydrateRow(array $data, array &$cache, array &$result) + { + $result[] = $this->_gatherScalarRowData($data, $cache); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php b/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php new file mode 100644 index 0000000..4668155 --- /dev/null +++ b/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Internal\Hydration; + +use Doctrine\DBAL\Connection; + +/** + * Hydrator that hydrates a single scalar value from the result set. + * + * @author Roman Borschel + * @since 2.0 + */ +class SingleScalarHydrator extends AbstractHydrator +{ + /** @override */ + protected function _hydrateAll() + { + $cache = array(); + $result = $this->_stmt->fetchAll(\PDO::FETCH_ASSOC); + $num = count($result); + + if ($num == 0) { + throw new \Doctrine\ORM\NoResultException; + } else if ($num > 1 || count($result[key($result)]) > 1) { + throw new \Doctrine\ORM\NonUniqueResultException; + } + + $result = $this->_gatherScalarRowData($result[key($result)], $cache); + + return array_shift($result); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/ClassMetadata.php b/Doctrine/ORM/Mapping/ClassMetadata.php new file mode 100644 index 0000000..15112ad --- /dev/null +++ b/Doctrine/ORM/Mapping/ClassMetadata.php @@ -0,0 +1,376 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use ReflectionClass, ReflectionProperty; + +/** + * A ClassMetadata instance holds all the object-relational mapping metadata + * of an entity and it's associations. + * + * Once populated, ClassMetadata instances are usually cached in a serialized form. + * + * IMPORTANT NOTE: + * + * The fields of this class are only public for 2 reasons: + * 1) To allow fast READ access. + * 2) To drastically reduce the size of a serialized instance (private/protected members + * get the whole class name, namespace inclusive, prepended to every property in + * the serialized representation). + * + * @author Roman Borschel + * @author Jonathan H. Wage + * @since 2.0 + */ +class ClassMetadata extends ClassMetadataInfo +{ + /** + * The ReflectionProperty instances of the mapped class. + * + * @var array + */ + public $reflFields = array(); + + /** + * The prototype from which new instances of the mapped class are created. + * + * @var object + */ + private $_prototype; + + /** + * Initializes a new ClassMetadata instance that will hold the object-relational mapping + * metadata of the class with the given name. + * + * @param string $entityName The name of the entity class the new instance is used for. + */ + public function __construct($entityName) + { + parent::__construct($entityName); + $this->reflClass = new ReflectionClass($entityName); + $this->namespace = $this->reflClass->getNamespaceName(); + $this->table['name'] = $this->reflClass->getShortName(); + } + + /** + * Gets the ReflectionPropertys of the mapped class. + * + * @return array An array of ReflectionProperty instances. + */ + public function getReflectionProperties() + { + return $this->reflFields; + } + + /** + * Gets a ReflectionProperty for a specific field of the mapped class. + * + * @param string $name + * @return ReflectionProperty + */ + public function getReflectionProperty($name) + { + return $this->reflFields[$name]; + } + + /** + * Gets the ReflectionProperty for the single identifier field. + * + * @return ReflectionProperty + * @throws BadMethodCallException If the class has a composite identifier. + */ + public function getSingleIdReflectionProperty() + { + if ($this->isIdentifierComposite) { + throw new \BadMethodCallException("Class " . $this->name . " has a composite identifier."); + } + return $this->reflFields[$this->identifier[0]]; + } + + /** + * Validates & completes the given field mapping. + * + * @param array $mapping The field mapping to validated & complete. + * @return array The validated and completed field mapping. + * + * @throws MappingException + */ + protected function _validateAndCompleteFieldMapping(array &$mapping) + { + parent::_validateAndCompleteFieldMapping($mapping); + + // Store ReflectionProperty of mapped field + $refProp = $this->reflClass->getProperty($mapping['fieldName']); + $refProp->setAccessible(true); + $this->reflFields[$mapping['fieldName']] = $refProp; + } + + /** + * Extracts the identifier values of an entity of this class. + * + * For composite identifiers, the identifier values are returned as an array + * with the same order as the field order in {@link identifier}. + * + * @param object $entity + * @return array + */ + public function getIdentifierValues($entity) + { + if ($this->isIdentifierComposite) { + $id = array(); + foreach ($this->identifier as $idField) { + $value = $this->reflFields[$idField]->getValue($entity); + if ($value !== null) { + $id[$idField] = $value; + } + } + return $id; + } else { + $value = $this->reflFields[$this->identifier[0]]->getValue($entity); + if ($value !== null) { + return array($this->identifier[0] => $value); + } + return array(); + } + } + + /** + * Populates the entity identifier of an entity. + * + * @param object $entity + * @param mixed $id + * @todo Rename to assignIdentifier() + */ + public function setIdentifierValues($entity, array $id) + { + foreach ($id as $idField => $idValue) { + $this->reflFields[$idField]->setValue($entity, $idValue); + } + } + + /** + * Sets the specified field to the specified value on the given entity. + * + * @param object $entity + * @param string $field + * @param mixed $value + */ + public function setFieldValue($entity, $field, $value) + { + $this->reflFields[$field]->setValue($entity, $value); + } + + /** + * Gets the specified field's value off the given entity. + * + * @param object $entity + * @param string $field + */ + public function getFieldValue($entity, $field) + { + return $this->reflFields[$field]->getValue($entity); + } + + /** + * Stores the association mapping. + * + * @param AssociationMapping $assocMapping + */ + protected function _storeAssociationMapping(array $assocMapping) + { + parent::_storeAssociationMapping($assocMapping); + + // Store ReflectionProperty of mapped field + $sourceFieldName = $assocMapping['fieldName']; + + $refProp = $this->reflClass->getProperty($sourceFieldName); + $refProp->setAccessible(true); + $this->reflFields[$sourceFieldName] = $refProp; + } + + /** + * Gets the (possibly quoted) column name of a mapped field for safe use + * in an SQL statement. + * + * @param string $field + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedColumnName($field, $platform) + { + return isset($this->fieldMappings[$field]['quoted']) ? + $platform->quoteIdentifier($this->fieldMappings[$field]['columnName']) : + $this->fieldMappings[$field]['columnName']; + } + + /** + * Gets the (possibly quoted) primary table name of this class for safe use + * in an SQL statement. + * + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedTableName($platform) + { + return isset($this->table['quoted']) ? + $platform->quoteIdentifier($this->table['name']) : + $this->table['name']; + } + + /** + * Gets the (possibly quoted) name of the join table. + * + * @param AbstractPlatform $platform + * @return string + */ + public function getQuotedJoinTableName(array $assoc, $platform) + { + return isset($assoc['joinTable']['quoted']) + ? $platform->quoteIdentifier($assoc['joinTable']['name']) + : $assoc['joinTable']['name']; + } + + /** + * Creates a string representation of this instance. + * + * @return string The string representation of this instance. + * @todo Construct meaningful string representation. + */ + public function __toString() + { + return __CLASS__ . '@' . spl_object_hash($this); + } + + /** + * Determines which fields get serialized. + * + * It is only serialized what is necessary for best unserialization performance. + * That means any metadata properties that are not set or empty or simply have + * their default value are NOT serialized. + * + * Parts that are also NOT serialized because they can not be properly unserialized: + * - reflClass (ReflectionClass) + * - reflFields (ReflectionProperty array) + * + * @return array The names of all the fields that should be serialized. + */ + public function __sleep() + { + // This metadata is always serialized/cached. + $serialized = array( + 'associationMappings', + 'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName'] + 'fieldMappings', + 'fieldNames', + 'identifier', + 'isIdentifierComposite', // TODO: REMOVE + 'name', + 'namespace', // TODO: REMOVE + 'table', + 'rootEntityName', + 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime. + ); + + // The rest of the metadata is only serialized if necessary. + if ($this->changeTrackingPolicy != self::CHANGETRACKING_DEFERRED_IMPLICIT) { + $serialized[] = 'changeTrackingPolicy'; + } + + if ($this->customRepositoryClassName) { + $serialized[] = 'customRepositoryClassName'; + } + + if ($this->inheritanceType != self::INHERITANCE_TYPE_NONE) { + $serialized[] = 'inheritanceType'; + $serialized[] = 'discriminatorColumn'; + $serialized[] = 'discriminatorValue'; + $serialized[] = 'discriminatorMap'; + $serialized[] = 'parentClasses'; + $serialized[] = 'subClasses'; + } + + if ($this->generatorType != self::GENERATOR_TYPE_NONE) { + $serialized[] = 'generatorType'; + if ($this->generatorType == self::GENERATOR_TYPE_SEQUENCE) { + $serialized[] = 'sequenceGeneratorDefinition'; + } + } + + if ($this->isMappedSuperclass) { + $serialized[] = 'isMappedSuperclass'; + } + + if ($this->isVersioned) { + $serialized[] = 'isVersioned'; + $serialized[] = 'versionField'; + } + + if ($this->lifecycleCallbacks) { + $serialized[] = 'lifecycleCallbacks'; + } + + return $serialized; + } + + /** + * Restores some state that can not be serialized/unserialized. + * + * @return void + */ + public function __wakeup() + { + // Restore ReflectionClass and properties + $this->reflClass = new ReflectionClass($this->name); + + foreach ($this->fieldMappings as $field => $mapping) { + if (isset($mapping['declared'])) { + $reflField = new ReflectionProperty($mapping['declared'], $field); + } else { + $reflField = $this->reflClass->getProperty($field); + } + $reflField->setAccessible(true); + $this->reflFields[$field] = $reflField; + } + + foreach ($this->associationMappings as $field => $mapping) { + if (isset($mapping['declared'])) { + $reflField = new ReflectionProperty($mapping['declared'], $field); + } else { + $reflField = $this->reflClass->getProperty($field); + } + + $reflField->setAccessible(true); + $this->reflFields[$field] = $reflField; + } + } + + /** + * Creates a new instance of the mapped class, without invoking the constructor. + * + * @return object + */ + public function newInstance() + { + if ($this->_prototype === null) { + $this->_prototype = unserialize(sprintf('O:%d:"%s":0:{}', strlen($this->name), $this->name)); + } + return clone $this->_prototype; + } +} diff --git a/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/Doctrine/ORM/Mapping/ClassMetadataFactory.php new file mode 100644 index 0000000..174e95e --- /dev/null +++ b/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -0,0 +1,455 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use ReflectionException, + Doctrine\ORM\ORMException, + Doctrine\ORM\EntityManager, + Doctrine\DBAL\Platforms, + Doctrine\ORM\Events; + +/** + * The ClassMetadataFactory is used to create ClassMetadata objects that contain all the + * metadata mapping informations of a class which describes how a class should be mapped + * to a relational database. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ClassMetadataFactory +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @var AbstractPlatform + */ + private $targetPlatform; + + /** + * @var Driver\Driver + */ + private $driver; + + /** + * @var \Doctrine\Common\EventManager + */ + private $evm; + + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $cacheDriver; + + /** + * @var array + */ + private $loadedMetadata = array(); + + /** + * @var bool + */ + private $initialized = false; + + /** + * @param EntityManager $$em + */ + public function setEntityManager(EntityManager $em) + { + $this->em = $em; + } + + /** + * Sets the cache driver used by the factory to cache ClassMetadata instances. + * + * @param Doctrine\Common\Cache\Cache $cacheDriver + */ + public function setCacheDriver($cacheDriver) + { + $this->cacheDriver = $cacheDriver; + } + + /** + * Gets the cache driver used by the factory to cache ClassMetadata instances. + * + * @return Doctrine\Common\Cache\Cache + */ + public function getCacheDriver() + { + return $this->cacheDriver; + } + + public function getLoadedMetadata() + { + return $this->loadedMetadata; + } + + /** + * Forces the factory to load the metadata of all classes known to the underlying + * mapping driver. + * + * @return array The ClassMetadata instances of all mapped classes. + */ + public function getAllMetadata() + { + if ( ! $this->initialized) { + $this->initialize(); + } + + $metadata = array(); + foreach ($this->driver->getAllClassNames() as $className) { + $metadata[] = $this->getMetadataFor($className); + } + + return $metadata; + } + + /** + * Lazy initialization of this stuff, especially the metadata driver, + * since these are not needed at all when a metadata cache is active. + */ + private function initialize() + { + $this->driver = $this->em->getConfiguration()->getMetadataDriverImpl(); + $this->targetPlatform = $this->em->getConnection()->getDatabasePlatform(); + $this->evm = $this->em->getEventManager(); + $this->initialized = true; + } + + /** + * Gets the class metadata descriptor for a class. + * + * @param string $className The name of the class. + * @return Doctrine\ORM\Mapping\ClassMetadata + */ + public function getMetadataFor($className) + { + if ( ! isset($this->loadedMetadata[$className])) { + $realClassName = $className; + + // Check for namespace alias + if (strpos($className, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $className); + $realClassName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; + + if (isset($this->loadedMetadata[$realClassName])) { + // We do not have the alias name in the map, include it + $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; + + return $this->loadedMetadata[$realClassName]; + } + } + + if ($this->cacheDriver) { + if (($cached = $this->cacheDriver->fetch("$realClassName\$CLASSMETADATA")) !== false) { + $this->loadedMetadata[$realClassName] = $cached; + } else { + foreach ($this->loadMetadata($realClassName) as $loadedClassName) { + $this->cacheDriver->save( + "$loadedClassName\$CLASSMETADATA", $this->loadedMetadata[$loadedClassName], null + ); + } + } + } else { + $this->loadMetadata($realClassName); + } + + if ($className != $realClassName) { + // We do not have the alias name in the map, include it + $this->loadedMetadata[$className] = $this->loadedMetadata[$realClassName]; + } + } + + return $this->loadedMetadata[$className]; + } + + /** + * Checks whether the factory has the metadata for a class loaded already. + * + * @param string $className + * @return boolean TRUE if the metadata of the class in question is already loaded, FALSE otherwise. + */ + public function hasMetadataFor($className) + { + return isset($this->loadedMetadata[$className]); + } + + /** + * Sets the metadata descriptor for a specific class. + * + * NOTE: This is only useful in very special cases, like when generating proxy classes. + * + * @param string $className + * @param ClassMetadata $class + */ + public function setMetadataFor($className, $class) + { + $this->loadedMetadata[$className] = $class; + } + + /** + * Get array of parent classes for the given entity class + * + * @param string $name + * @return array $parentClasses + */ + protected function getParentClasses($name) + { + // Collect parent classes, ignoring transient (not-mapped) classes. + $parentClasses = array(); + foreach (array_reverse(class_parents($name)) as $parentClass) { + if ( ! $this->driver->isTransient($parentClass)) { + $parentClasses[] = $parentClass; + } + } + return $parentClasses; + } + + /** + * Loads the metadata of the class in question and all it's ancestors whose metadata + * is still not loaded. + * + * @param string $name The name of the class for which the metadata should get loaded. + * @param array $tables The metadata collection to which the loaded metadata is added. + */ + protected function loadMetadata($name) + { + if ( ! $this->initialized) { + $this->initialize(); + } + + $loaded = array(); + + $parentClasses = $this->getParentClasses($name); + $parentClasses[] = $name; + + // Move down the hierarchy of parent classes, starting from the topmost class + $parent = null; + $visited = array(); + foreach ($parentClasses as $className) { + if (isset($this->loadedMetadata[$className])) { + $parent = $this->loadedMetadata[$className]; + if ( ! $parent->isMappedSuperclass) { + array_unshift($visited, $className); + } + continue; + } + + $class = $this->newClassMetadataInstance($className); + + if ($parent) { + if (!$parent->isMappedSuperclass) { + $class->setInheritanceType($parent->inheritanceType); + $class->setDiscriminatorColumn($parent->discriminatorColumn); + } + $class->setIdGeneratorType($parent->generatorType); + $this->addInheritedFields($class, $parent); + $this->addInheritedRelations($class, $parent); + $class->setIdentifier($parent->identifier); + $class->setVersioned($parent->isVersioned); + $class->setVersionField($parent->versionField); + if (!$parent->isMappedSuperclass) { + $class->setDiscriminatorMap($parent->discriminatorMap); + } + $class->setLifecycleCallbacks($parent->lifecycleCallbacks); + $class->setChangeTrackingPolicy($parent->changeTrackingPolicy); + } + + // Invoke driver + try { + $this->driver->loadMetadataForClass($className, $class); + } catch (ReflectionException $e) { + throw MappingException::reflectionFailure($className, $e); + } + + // Verify & complete identifier mapping + if ( ! $class->identifier && ! $class->isMappedSuperclass) { + throw MappingException::identifierRequired($className); + } + if ($parent && ! $parent->isMappedSuperclass) { + if ($parent->isIdGeneratorSequence()) { + $class->setSequenceGeneratorDefinition($parent->sequenceGeneratorDefinition); + } else if ($parent->isIdGeneratorTable()) { + $class->getTableGeneratorDefinition($parent->tableGeneratorDefinition); + } + if ($parent->generatorType) { + $class->setIdGeneratorType($parent->generatorType); + } + if ($parent->idGenerator) { + $class->setIdGenerator($parent->idGenerator); + } + } else { + $this->completeIdGeneratorMapping($class); + } + + if ($parent && $parent->isInheritanceTypeSingleTable()) { + $class->setPrimaryTable($parent->table); + } + + $class->setParentClasses($visited); + + if ($this->evm->hasListeners(Events::loadClassMetadata)) { + $eventArgs = new \Doctrine\ORM\Event\LoadClassMetadataEventArgs($class, $this->em); + $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); + } + + // verify inheritance + if (!$parent && !$class->isMappedSuperclass && !$class->isInheritanceTypeNone()) { + if (count($class->discriminatorMap) == 0) { + throw MappingException::missingDiscriminatorMap($class->name); + } + if (!$class->discriminatorColumn) { + throw MappingException::missingDiscriminatorColumn($class->name); + } + } + + $this->loadedMetadata[$className] = $class; + + $parent = $class; + + if ( ! $class->isMappedSuperclass) { + array_unshift($visited, $className); + } + + $loaded[] = $className; + } + + return $loaded; + } + + /** + * Creates a new ClassMetadata instance for the given class name. + * + * @param string $className + * @return Doctrine\ORM\Mapping\ClassMetadata + */ + protected function newClassMetadataInstance($className) + { + return new ClassMetadata($className); + } + + /** + * Adds inherited fields to the subclass mapping. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $subClass + * @param Doctrine\ORM\Mapping\ClassMetadata $parentClass + */ + private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass) + { + foreach ($parentClass->fieldMappings as $fieldName => $mapping) { + if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { + $mapping['inherited'] = $parentClass->name; + } + if ( ! isset($mapping['declared'])) { + $mapping['declared'] = $parentClass->name; + } + $subClass->addInheritedFieldMapping($mapping); + } + foreach ($parentClass->reflFields as $name => $field) { + $subClass->reflFields[$name] = $field; + } + } + + /** + * Adds inherited association mappings to the subclass mapping. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $subClass + * @param Doctrine\ORM\Mapping\ClassMetadata $parentClass + */ + private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass) + { + foreach ($parentClass->associationMappings as $field => $mapping) { + if ($parentClass->isMappedSuperclass) { + $mapping['sourceEntity'] = $subClass->name; + } + + //$subclassMapping = $mapping; + if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) { + $mapping['inherited'] = $parentClass->name; + } + if ( ! isset($mapping['declared'])) { + $mapping['declared'] = $parentClass->name; + } + $subClass->addInheritedAssociationMapping($mapping); + } + } + + /** + * Completes the ID generator mapping. If "auto" is specified we choose the generator + * most appropriate for the targeted database platform. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + */ + private function completeIdGeneratorMapping(ClassMetadataInfo $class) + { + $idGenType = $class->generatorType; + if ($idGenType == ClassMetadata::GENERATOR_TYPE_AUTO) { + if ($this->targetPlatform->prefersSequences()) { + $class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_SEQUENCE); + } else if ($this->targetPlatform->prefersIdentityColumns()) { + $class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY); + } else { + $class->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_TABLE); + } + } + + // Create & assign an appropriate ID generator instance + switch ($class->generatorType) { + case ClassMetadata::GENERATOR_TYPE_IDENTITY: + // For PostgreSQL IDENTITY (SERIAL) we need a sequence name. It defaults to + // __seq in PostgreSQL for SERIAL columns. + // Not pretty but necessary and the simplest solution that currently works. + $seqName = $this->targetPlatform instanceof Platforms\PostgreSQLPlatform ? + $class->table['name'] . '_' . $class->columnNames[$class->identifier[0]] . '_seq' : + null; + $class->setIdGenerator(new \Doctrine\ORM\Id\IdentityGenerator($seqName)); + break; + case ClassMetadata::GENERATOR_TYPE_SEQUENCE: + // If there is no sequence definition yet, create a default definition + $definition = $class->sequenceGeneratorDefinition; + if ( ! $definition) { + $sequenceName = $class->getTableName() . '_' . $class->getSingleIdentifierColumnName() . '_seq'; + $definition['sequenceName'] = $this->targetPlatform->fixSchemaElementName($sequenceName); + $definition['allocationSize'] = 1; + $definition['initialValue'] = 1; + $class->setSequenceGeneratorDefinition($definition); + } + $sequenceGenerator = new \Doctrine\ORM\Id\SequenceGenerator( + $definition['sequenceName'], + $definition['allocationSize'] + ); + $class->setIdGenerator($sequenceGenerator); + break; + case ClassMetadata::GENERATOR_TYPE_NONE: + $class->setIdGenerator(new \Doctrine\ORM\Id\AssignedGenerator()); + break; + case ClassMetadata::GENERATOR_TYPE_TABLE: + throw new ORMException("TableGenerator not yet implemented."); + break; + default: + throw new ORMException("Unknown generator type: " . $class->generatorType); + } + } +} diff --git a/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/Doctrine/ORM/Mapping/ClassMetadataInfo.php new file mode 100644 index 0000000..2d6ec7c --- /dev/null +++ b/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -0,0 +1,1595 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use ReflectionClass; + +/** + * A ClassMetadata instance holds all the object-relational mapping metadata + * of an entity and it's associations. + * + * Once populated, ClassMetadata instances are usually cached in a serialized form. + * + * IMPORTANT NOTE: + * + * The fields of this class are only public for 2 reasons: + * 1) To allow fast READ access. + * 2) To drastically reduce the size of a serialized instance (private/protected members + * get the whole class name, namespace inclusive, prepended to every property in + * the serialized representation). + * + * @author Roman Borschel + * @author Jonathan H. Wage + * @since 2.0 + */ +class ClassMetadataInfo +{ + /* The inheritance mapping types */ + /** + * NONE means the class does not participate in an inheritance hierarchy + * and therefore does not need an inheritance mapping type. + */ + const INHERITANCE_TYPE_NONE = 1; + /** + * JOINED means the class will be persisted according to the rules of + * Class Table Inheritance. + */ + const INHERITANCE_TYPE_JOINED = 2; + /** + * SINGLE_TABLE means the class will be persisted according to the rules of + * Single Table Inheritance. + */ + const INHERITANCE_TYPE_SINGLE_TABLE = 3; + /** + * TABLE_PER_CLASS means the class will be persisted according to the rules + * of Concrete Table Inheritance. + */ + const INHERITANCE_TYPE_TABLE_PER_CLASS = 4; + + /* The Id generator types. */ + /** + * AUTO means the generator type will depend on what the used platform prefers. + * Offers full portability. + */ + const GENERATOR_TYPE_AUTO = 1; + /** + * SEQUENCE means a separate sequence object will be used. Platforms that do + * not have native sequence support may emulate it. Full portability is currently + * not guaranteed. + */ + const GENERATOR_TYPE_SEQUENCE = 2; + /** + * TABLE means a separate table is used for id generation. + * Offers full portability. + */ + const GENERATOR_TYPE_TABLE = 3; + /** + * IDENTITY means an identity column is used for id generation. The database + * will fill in the id column on insertion. Platforms that do not support + * native identity columns may emulate them. Full portability is currently + * not guaranteed. + */ + const GENERATOR_TYPE_IDENTITY = 4; + /** + * NONE means the class does not have a generated id. That means the class + * must have a natural, manually assigned id. + */ + const GENERATOR_TYPE_NONE = 5; + /** + * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time + * by doing a property-by-property comparison with the original data. This will + * be done for all entities that are in MANAGED state at commit-time. + * + * This is the default change tracking policy. + */ + const CHANGETRACKING_DEFERRED_IMPLICIT = 1; + /** + * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time + * by doing a property-by-property comparison with the original data. This will + * be done only for entities that were explicitly saved (through persist() or a cascade). + */ + const CHANGETRACKING_DEFERRED_EXPLICIT = 2; + /** + * NOTIFY means that Doctrine relies on the entities sending out notifications + * when their properties change. Such entity classes must implement + * the NotifyPropertyChanged interface. + */ + const CHANGETRACKING_NOTIFY = 3; + /** + * Specifies that an association is to be fetched when it is first accessed. + */ + const FETCH_LAZY = 2; + /** + * Specifies that an association is to be fetched when the owner of the + * association is fetched. + */ + const FETCH_EAGER = 3; + /** + * Identifies a one-to-one association. + */ + const ONE_TO_ONE = 1; + /** + * Identifies a many-to-one association. + */ + const MANY_TO_ONE = 2; + /** + * Combined bitmask for to-one (single-valued) associations. + */ + const TO_ONE = 3; + /** + * Identifies a one-to-many association. + */ + const ONE_TO_MANY = 4; + /** + * Identifies a many-to-many association. + */ + const MANY_TO_MANY = 8; + /** + * Combined bitmask for to-many (collection-valued) associations. + */ + const TO_MANY = 12; + + /** + * READ-ONLY: The name of the entity class. + */ + public $name; + + /** + * READ-ONLY: The namespace the entity class is contained in. + * + * @var string + * @todo Not really needed. Usage could be localized. + */ + public $namespace; + + /** + * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance + * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same + * as {@link $entityName}. + * + * @var string + */ + public $rootEntityName; + + /** + * The name of the custom repository class used for the entity class. + * (Optional). + * + * @var string + */ + public $customRepositoryClassName; + + /** + * READ-ONLY: Whether this class describes the mapping of a mapped superclass. + * + * @var boolean + */ + public $isMappedSuperclass = false; + + /** + * READ-ONLY: The names of the parent classes (ancestors). + * + * @var array + */ + public $parentClasses = array(); + + /** + * READ-ONLY: The names of all subclasses (descendants). + * + * @var array + */ + public $subClasses = array(); + + /** + * READ-ONLY: The field names of all fields that are part of the identifier/primary key + * of the mapped entity class. + * + * @var array + */ + public $identifier = array(); + + /** + * READ-ONLY: The inheritance mapping type used by the class. + * + * @var integer + */ + public $inheritanceType = self::INHERITANCE_TYPE_NONE; + + /** + * READ-ONLY: The Id generator type used by the class. + * + * @var string + */ + public $generatorType = self::GENERATOR_TYPE_NONE; + + /** + * READ-ONLY: The field mappings of the class. + * Keys are field names and values are mapping definitions. + * + * The mapping definition array has the following values: + * + * - fieldName (string) + * The name of the field in the Entity. + * + * - type (string) + * The type name of the mapped field. Can be one of Doctrine's mapping types + * or a custom mapping type. + * + * - columnName (string, optional) + * The column name. Optional. Defaults to the field name. + * + * - length (integer, optional) + * The database length of the column. Optional. Default value taken from + * the type. + * + * - 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. + * + * - nullable (boolean, optional) + * Whether the column is nullable. Defaults to FALSE. + * + * - columnDefinition (string, optional, schema-only) + * The SQL fragment that is used when generating the DDL for the column. + * + * - precision (integer, optional, schema-only) + * The precision of a decimal column. Only valid if the column type is decimal. + * + * - scale (integer, optional, schema-only) + * The scale of a decimal column. Only valid if the column type is decimal. + * + * - unique (string, optional, schema-only) + * Whether a unique constraint should be generated for the column. + * + * @var array + */ + public $fieldMappings = array(); + + /** + * READ-ONLY: An array of field names. Used to look up field names from column names. + * Keys are column names and values are field names. + * This is the reverse lookup map of $_columnNames. + * + * @var array + */ + public $fieldNames = array(); + + /** + * READ-ONLY: A map of field names to column names. Keys are field names and values column names. + * Used to look up column names from field names. + * This is the reverse lookup map of $_fieldNames. + * + * @var array + * @todo We could get rid of this array by just using $fieldMappings[$fieldName]['columnName']. + */ + public $columnNames = array(); + + /** + * READ-ONLY: The discriminator value of this class. + * + * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies + * where a discriminator column is used. + * + * @var mixed + * @see discriminatorColumn + */ + public $discriminatorValue; + + /** + * READ-ONLY: The discriminator map of all mapped classes in the hierarchy. + * + * This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies + * where a discriminator column is used. + * + * @var mixed + * @see discriminatorColumn + */ + public $discriminatorMap = array(); + + /** + * READ-ONLY: The definition of the descriminator column used in JOINED and SINGLE_TABLE + * inheritance mappings. + * + * @var array + */ + public $discriminatorColumn; + + /** + * READ-ONLY: The primary table definition. The definition is an array with the + * following entries: + * + * name => + * schema => + * indexes => array + * uniqueConstraints => array + * + * @var array + * @todo Rename to just $table + */ + public $table; + + /** + * READ-ONLY: The registered lifecycle callbacks for entities of this class. + * + * @var array + */ + public $lifecycleCallbacks = array(); + + /** + * READ-ONLY: The association mappings of this class. + * + * The mapping definition array supports the following keys: + * + * - fieldName (string) + * The name of the field in the entity the association is mapped to. + * + * - targetEntity (string) + * The class name of the target entity. If it is fully-qualified it is used as is. + * If it is a simple, unqualified class name the namespace is assumed to be the same + * as the namespace of the source entity. + * + * - mappedBy (string, required for bidirectional associations) + * The name of the field that completes the bidirectional association on the owning side. + * This key must be specified on the inverse side of a bidirectional association. + * + * - inversedBy (string, required for bidirectional associations) + * The name of the field that completes the bidirectional association on the inverse side. + * This key must be specified on the owning side of a bidirectional association. + * + * - cascade (array, optional) + * The names of persistence operations to cascade on the association. The set of possible + * values are: "persist", "remove", "detach", "merge", "refresh", "all" (implies all others). + * + * - orderBy (array, one-to-many/many-to-many only) + * A map of field names (of the target entity) to sorting directions (ASC/DESC). + * Example: array('priority' => 'desc') + * + * - fetch (integer, optional) + * The fetching strategy to use for the association, usually defaults to FETCH_LAZY. + * Possible values are: ClassMetadata::FETCH_EAGER, ClassMetadata::FETCH_LAZY. + * + * - joinTable (array, optional, many-to-many only) + * Specification of the join table and its join columns (foreign keys). + * Only valid for many-to-many mappings. Note that one-to-many associations can be mapped + * through a join table by simply mapping the association as many-to-many with a unique + * constraint on the join table. + * + * A join table definition has the following structure: + *
+     * array(
+     *     'name' => ,
+     *      'joinColumns' => array(),
+     *      'inverseJoinColumns' => array()
+     * )
+     * 
+ * + * + * @var array + */ + public $associationMappings = array(); + + /** + * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite. + * + * @var boolean + */ + public $isIdentifierComposite = false; + + /** + * READ-ONLY: The ID generator used for generating IDs for this class. + * + * @var AbstractIdGenerator + * @todo Remove! + */ + public $idGenerator; + + /** + * READ-ONLY: The definition of the sequence generator of this class. Only used for the + * SEQUENCE generation strategy. + * + * The definition has the following structure: + * + * array( + * 'sequenceName' => 'name', + * 'allocationSize' => 20, + * 'initialValue' => 1 + * ) + * + * + * @var array + * @todo Merge with tableGeneratorDefinition into generic generatorDefinition + */ + public $sequenceGeneratorDefinition; + + /** + * READ-ONLY: The definition of the table generator of this class. Only used for the + * TABLE generation strategy. + * + * @var array + * @todo Merge with tableGeneratorDefinition into generic generatorDefinition + */ + public $tableGeneratorDefinition; + + /** + * READ-ONLY: The policy used for change-tracking on entities of this class. + * + * @var integer + */ + public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT; + + /** + * READ-ONLY: A flag for whether or not instances of this class are to be versioned + * with optimistic locking. + * + * @var boolean $isVersioned + */ + public $isVersioned; + + /** + * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any). + * + * @var mixed $versionField + */ + public $versionField; + + /** + * The ReflectionClass instance of the mapped class. + * + * @var ReflectionClass + */ + public $reflClass; + + /** + * Initializes a new ClassMetadata instance that will hold the object-relational mapping + * metadata of the class with the given name. + * + * @param string $entityName The name of the entity class the new instance is used for. + */ + public function __construct($entityName) + { + $this->name = $entityName; + $this->rootEntityName = $entityName; + } + + /** + * Gets the ReflectionClass instance of the mapped class. + * + * @return ReflectionClass + */ + public function getReflectionClass() + { + if ( ! $this->reflClass) { + $this->reflClass = new ReflectionClass($this->name); + } + return $this->reflClass; + } + + /** + * Sets the change tracking policy used by this class. + * + * @param integer $policy + */ + public function setChangeTrackingPolicy($policy) + { + $this->changeTrackingPolicy = $policy; + } + + /** + * Whether the change tracking policy of this class is "deferred explicit". + * + * @return boolean + */ + public function isChangeTrackingDeferredExplicit() + { + return $this->changeTrackingPolicy == self::CHANGETRACKING_DEFERRED_EXPLICIT; + } + + /** + * Whether the change tracking policy of this class is "deferred implicit". + * + * @return boolean + */ + public function isChangeTrackingDeferredImplicit() + { + return $this->changeTrackingPolicy == self::CHANGETRACKING_DEFERRED_IMPLICIT; + } + + /** + * Whether the change tracking policy of this class is "notify". + * + * @return boolean + */ + public function isChangeTrackingNotify() + { + return $this->changeTrackingPolicy == self::CHANGETRACKING_NOTIFY; + } + + /** + * Checks whether a field is part of the identifier/primary key field(s). + * + * @param string $fieldName The field name + * @return boolean TRUE if the field is part of the table identifier/primary key field(s), + * FALSE otherwise. + */ + public function isIdentifier($fieldName) + { + if ( ! $this->isIdentifierComposite) { + return $fieldName === $this->identifier[0]; + } + return in_array($fieldName, $this->identifier); + } + + /** + * Check if the field is unique. + * + * @param string $fieldName The field name + * @return boolean TRUE if the field is unique, FALSE otherwise. + */ + public function isUniqueField($fieldName) + { + $mapping = $this->getFieldMapping($fieldName); + if ($mapping !== false) { + return isset($mapping['unique']) && $mapping['unique'] == true; + } + return false; + } + + /** + * Check if the field is not null. + * + * @param string $fieldName The field name + * @return boolean TRUE if the field is not null, FALSE otherwise. + */ + public function isNullable($fieldName) + { + $mapping = $this->getFieldMapping($fieldName); + if ($mapping !== false) { + return isset($mapping['nullable']) && $mapping['nullable'] == true; + } + return false; + } + + /** + * Gets a column name for a field name. + * If the column name for the field cannot be found, the given field name + * is returned. + * + * @param string $fieldName The field name. + * @return string The column name. + */ + public function getColumnName($fieldName) + { + return isset($this->columnNames[$fieldName]) ? + $this->columnNames[$fieldName] : $fieldName; + } + + /** + * Gets the mapping of a (regular) field that holds some data but not a + * reference to another object. + * + * @param string $fieldName The field name. + * @return array The field mapping. + */ + public function getFieldMapping($fieldName) + { + if ( ! isset($this->fieldMappings[$fieldName])) { + throw MappingException::mappingNotFound($this->name, $fieldName); + } + return $this->fieldMappings[$fieldName]; + } + + /** + * Gets the mapping of an association. + * + * @see ClassMetadataInfo::$associationMappings + * @param string $fieldName The field name that represents the association in + * the object model. + * @return array The mapping. + */ + public function getAssociationMapping($fieldName) + { + if ( ! isset($this->associationMappings[$fieldName])) { + throw MappingException::mappingNotFound($this->name, $fieldName); + } + return $this->associationMappings[$fieldName]; + } + + /** + * Gets all association mappings of the class. + * + * @return array + */ + public function getAssociationMappings() + { + return $this->associationMappings; + } + + /** + * Gets the field name for a column name. + * If no field name can be found the column name is returned. + * + * @param string $columnName column name + * @return string column alias + */ + public function getFieldName($columnName) + { + return isset($this->fieldNames[$columnName]) ? + $this->fieldNames[$columnName] : $columnName; + } + + /** + * Validates & completes the given field mapping. + * + * @param array $mapping The field mapping to validated & complete. + * @return array The validated and completed field mapping. + */ + protected function _validateAndCompleteFieldMapping(array &$mapping) + { + // Check mandatory fields + if ( ! isset($mapping['fieldName'])) { + throw MappingException::missingFieldName($this->name, $mapping); + } + if ( ! isset($mapping['type'])) { + // Default to string + $mapping['type'] = 'string'; + } + + // Complete fieldName and columnName mapping + if ( ! isset($mapping['columnName'])) { + $mapping['columnName'] = $mapping['fieldName']; + } else { + if ($mapping['columnName'][0] == '`') { + $mapping['columnName'] = trim($mapping['columnName'], '`'); + $mapping['quoted'] = true; + } + } + + $this->columnNames[$mapping['fieldName']] = $mapping['columnName']; + if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorColumn != null && $this->discriminatorColumn['name'] == $mapping['columnName'])) { + throw MappingException::duplicateColumnName($this->name, $mapping['columnName']); + } + + $this->fieldNames[$mapping['columnName']] = $mapping['fieldName']; + + // Complete id mapping + if (isset($mapping['id']) && $mapping['id'] === true) { + if ($this->versionField == $mapping['fieldName']) { + throw MappingException::cannotVersionIdField($this->name, $mapping['fieldName']); + } + + if ( ! in_array($mapping['fieldName'], $this->identifier)) { + $this->identifier[] = $mapping['fieldName']; + } + // Check for composite key + if ( ! $this->isIdentifierComposite && count($this->identifier) > 1) { + $this->isIdentifierComposite = true; + } + } + } + + /** + * Validates & completes the basic mapping information that is common to all + * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many). + * + * @param array $mapping The mapping. + * @return array The updated mapping. + * @throws MappingException If something is wrong with the mapping. + */ + protected function _validateAndCompleteAssociationMapping(array $mapping) + { + if ( ! isset($mapping['mappedBy'])) { + $mapping['mappedBy'] = null; + } + if ( ! isset($mapping['inversedBy'])) { + $mapping['inversedBy'] = null; + } + $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy + + // If targetEntity is unqualified, assume it is in the same namespace as + // the sourceEntity. + $mapping['sourceEntity'] = $this->name; + if (isset($mapping['targetEntity']) && strpos($mapping['targetEntity'], '\\') === false + && strlen($this->namespace) > 0) { + $mapping['targetEntity'] = $this->namespace . '\\' . $mapping['targetEntity']; + } + + // Mandatory: fieldName, targetEntity + if ( ! isset($mapping['fieldName'])) { + throw MappingException::missingFieldName(); + } + if ( ! isset($mapping['targetEntity'])) { + throw MappingException::missingTargetEntity($mapping['fieldName']); + } + + // Mandatory and optional attributes for either side + if ( ! $mapping['mappedBy']) { + if (isset($mapping['joinTable']) && $mapping['joinTable']) { + if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] == '`') { + $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`'); + $mapping['joinTable']['quoted'] = true; + } + } + } else { + $mapping['isOwningSide'] = false; + } + + // Fetch mode. Default fetch mode to LAZY, if not set. + if ( ! isset($mapping['fetch'])) { + $mapping['fetch'] = self::FETCH_LAZY; + } + + // Cascades + $cascades = isset($mapping['cascade']) ? $mapping['cascade'] : array(); + if (in_array('all', $cascades)) { + $cascades = array( + 'remove', + 'persist', + 'refresh', + 'merge', + 'detach' + ); + } + $mapping['cascade'] = $cascades; + $mapping['isCascadeRemove'] = in_array('remove', $cascades); + $mapping['isCascadePersist'] = in_array('persist', $cascades); + $mapping['isCascadeRefresh'] = in_array('refresh', $cascades); + $mapping['isCascadeMerge'] = in_array('merge', $cascades); + $mapping['isCascadeDetach'] = in_array('detach', $cascades); + + return $mapping; + } + + /** + * Validates & completes a one-to-one association mapping. + * + * @param array $mapping The mapping to validate & complete. + * @return array The validated & completed mapping. + * @override + */ + protected function _validateAndCompleteOneToOneMapping(array $mapping) + { + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + if (isset($mapping['joinColumns']) && $mapping['joinColumns']) { + $mapping['isOwningSide'] = true; + } + + if ($mapping['isOwningSide']) { + if ( ! isset($mapping['joinColumns']) || ! $mapping['joinColumns']) { + // Apply default join column + $mapping['joinColumns'] = array(array( + 'name' => $mapping['fieldName'] . '_id', + 'referencedColumnName' => 'id' + )); + } + foreach ($mapping['joinColumns'] as $key => &$joinColumn) { + if ($mapping['type'] === self::ONE_TO_ONE) { + $joinColumn['unique'] = true; + } + if (empty($joinColumn['name'])) { + $joinColumn['name'] = $mapping['fieldName'] . '_id'; + } + if (empty($joinColumn['referencedColumnName'])) { + $joinColumn['referencedColumnName'] = 'id'; + } + $mapping['sourceToTargetKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName']; + $mapping['joinColumnFieldNames'][$joinColumn['name']] = isset($joinColumn['fieldName']) + ? $joinColumn['fieldName'] : $joinColumn['name']; + } + $mapping['targetToSourceKeyColumns'] = array_flip($mapping['sourceToTargetKeyColumns']); + } + + //TODO: if orphanRemoval, cascade=remove is implicit! + $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ? + (bool) $mapping['orphanRemoval'] : false; + + return $mapping; + } + + /** + * Validates and completes the mapping. + * + * @param array $mapping The mapping to validate and complete. + * @return array The validated and completed mapping. + * @override + */ + protected function _validateAndCompleteOneToManyMapping(array $mapping) + { + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + + // OneToMany-side MUST be inverse (must have mappedBy) + if ( ! isset($mapping['mappedBy'])) { + throw MappingException::oneToManyRequiresMappedBy($mapping['fieldName']); + } + + //TODO: if orphanRemoval, cascade=remove is implicit! + $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ? + (bool) $mapping['orphanRemoval'] : false; + + if (isset($mapping['orderBy'])) { + if ( ! is_array($mapping['orderBy'])) { + throw new \InvalidArgumentException("'orderBy' is expected to be an array, not ".gettype($mapping['orderBy'])); + } + } + + return $mapping; + } + + protected function _validateAndCompleteManyToManyMapping(array $mapping) + { + $mapping = $this->_validateAndCompleteAssociationMapping($mapping); + if ($mapping['isOwningSide']) { + $sourceShortName = strtolower(substr($mapping['sourceEntity'], strrpos($mapping['sourceEntity'], '\\') + 1)); + $targetShortName = strtolower(substr($mapping['targetEntity'], strrpos($mapping['targetEntity'], '\\') + 1)); + // owning side MUST have a join table + if ( ! isset($mapping['joinTable']['name'])) { + $mapping['joinTable']['name'] = $sourceShortName .'_' . $targetShortName; + } + if ( ! isset($mapping['joinTable']['joinColumns'])) { + $mapping['joinTable']['joinColumns'] = array(array( + 'name' => $sourceShortName . '_id', + 'referencedColumnName' => 'id', + 'onDelete' => 'CASCADE')); + } + if ( ! isset($mapping['joinTable']['inverseJoinColumns'])) { + $mapping['joinTable']['inverseJoinColumns'] = array(array( + 'name' => $targetShortName . '_id', + 'referencedColumnName' => 'id', + 'onDelete' => 'CASCADE')); + } + + foreach ($mapping['joinTable']['joinColumns'] as &$joinColumn) { + if (empty($joinColumn['name'])) { + $joinColumn['name'] = $sourceShortName . '_id'; + } + if (empty($joinColumn['referencedColumnName'])) { + $joinColumn['referencedColumnName'] = 'id'; + } + if (isset($joinColumn['onDelete']) && strtolower($joinColumn['onDelete']) == 'cascade') { + $mapping['isOnDeleteCascade'] = true; + } + $mapping['relationToSourceKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName']; + $mapping['joinTableColumns'][] = $joinColumn['name']; + } + + foreach ($mapping['joinTable']['inverseJoinColumns'] as &$inverseJoinColumn) { + if (empty($inverseJoinColumn['name'])) { + $inverseJoinColumn['name'] = $targetShortName . '_id'; + } + if (empty($inverseJoinColumn['referencedColumnName'])) { + $inverseJoinColumn['referencedColumnName'] = 'id'; + } + if (isset($inverseJoinColumn['onDelete']) && strtolower($inverseJoinColumn['onDelete']) == 'cascade') { + $mapping['isOnDeleteCascade'] = true; + } + $mapping['relationToTargetKeyColumns'][$inverseJoinColumn['name']] = $inverseJoinColumn['referencedColumnName']; + $mapping['joinTableColumns'][] = $inverseJoinColumn['name']; + } + } + + if (isset($mapping['orderBy'])) { + if ( ! is_array($mapping['orderBy'])) { + throw new \InvalidArgumentException("'orderBy' is expected to be an array, not ".gettype($mapping['orderBy'])); + } + } + + return $mapping; + } + + /** + * Gets the identifier (primary key) field names of the class. + * + * @return mixed + */ + public function getIdentifierFieldNames() + { + 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 + * @throws MappingException If the class has a composite primary key. + */ + public function getSingleIdentifierFieldName() + { + if ($this->isIdentifierComposite) { + throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name); + } + return $this->identifier[0]; + } + + /** + * Gets the column name of the single id column. Note that this only works on + * entity classes that have a single-field pk. + * + * @return string + * @throws MappingException If the class has a composite primary key. + */ + public function getSingleIdentifierColumnName() + { + return $this->getColumnName($this->getSingleIdentifierFieldName()); + } + + /** + * INTERNAL: + * Sets the mapped identifier/primary key fields of this class. + * Mainly used by the ClassMetadataFactory to assign inherited identifiers. + * + * @param array $identifier + */ + public function setIdentifier(array $identifier) + { + $this->identifier = $identifier; + $this->isIdentifierComposite = (count($this->identifier) > 1); + } + + /** + * Checks whether the class has a (mapped) field with a certain name. + * + * @return boolean + */ + public function hasField($fieldName) + { + return isset($this->fieldMappings[$fieldName]); + } + + /** + * Gets an array containing all the column names. + * + * @return array + */ + public function getColumnNames(array $fieldNames = null) + { + if ($fieldNames === null) { + return array_keys($this->fieldNames); + } else { + $columnNames = array(); + foreach ($fieldNames as $fieldName) { + $columnNames[] = $this->getColumnName($fieldName); + } + return $columnNames; + } + } + + /** + * Returns an array with all the identifier column names. + * + * @return array + */ + public function getIdentifierColumnNames() + { + if ($this->isIdentifierComposite) { + $columnNames = array(); + foreach ($this->identifier as $idField) { + $columnNames[] = $this->fieldMappings[$idField]['columnName']; + } + return $columnNames; + } else { + return array($this->fieldMappings[$this->identifier[0]]['columnName']); + } + } + + /** + * Sets the type of Id generator to use for the mapped class. + */ + public function setIdGeneratorType($generatorType) + { + $this->generatorType = $generatorType; + } + + /** + * Checks whether the mapped class uses an Id generator. + * + * @return boolean TRUE if the mapped class uses an Id generator, FALSE otherwise. + */ + public function usesIdGenerator() + { + return $this->generatorType != self::GENERATOR_TYPE_NONE; + } + + /** + * @return boolean + */ + public function isInheritanceTypeNone() + { + return $this->inheritanceType == self::INHERITANCE_TYPE_NONE; + } + + /** + * Checks whether the mapped class uses the JOINED inheritance mapping strategy. + * + * @return boolean TRUE if the class participates in a JOINED inheritance mapping, + * FALSE otherwise. + */ + public function isInheritanceTypeJoined() + { + return $this->inheritanceType == self::INHERITANCE_TYPE_JOINED; + } + + /** + * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy. + * + * @return boolean TRUE if the class participates in a SINGLE_TABLE inheritance mapping, + * FALSE otherwise. + */ + public function isInheritanceTypeSingleTable() + { + return $this->inheritanceType == self::INHERITANCE_TYPE_SINGLE_TABLE; + } + + /** + * Checks whether the mapped class uses the TABLE_PER_CLASS inheritance mapping strategy. + * + * @return boolean TRUE if the class participates in a TABLE_PER_CLASS inheritance mapping, + * FALSE otherwise. + */ + public function isInheritanceTypeTablePerClass() + { + return $this->inheritanceType == self::INHERITANCE_TYPE_TABLE_PER_CLASS; + } + + /** + * 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. + * + * @param string $fieldName + * @return Doctrine\DBAL\Types\Type + */ + public function getTypeOfField($fieldName) + { + return isset($this->fieldMappings[$fieldName]) ? + $this->fieldMappings[$fieldName]['type'] : null; + } + + /** + * Gets the type of a column. + * + * @return Doctrine\DBAL\Types\Type + */ + public function getTypeOfColumn($columnName) + { + return $this->getTypeOfField($this->getFieldName($columnName)); + } + + /** + * Gets the name of the primary table. + * + * @return string + */ + public function getTableName() + { + return $this->table['name']; + } + + /** + * Gets the table name to use for temporary identifier tables of this class. + * + * @return string + */ + public function getTemporaryIdTableName() + { + return $this->table['name'] . '_id_tmp'; + } + + /** + * Sets the mapped subclasses of this class. + * + * @param array $subclasses The names of all mapped subclasses. + */ + public function setSubclasses(array $subclasses) + { + foreach ($subclasses as $subclass) { + if (strpos($subclass, '\\') === false && strlen($this->namespace)) { + $this->subClasses[] = $this->namespace . '\\' . $subclass; + } else { + $this->subClasses[] = $subclass; + } + } + } + + /** + * 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) + { + $this->parentClasses = $classNames; + if (count($classNames) > 0) { + $this->rootEntityName = array_pop($classNames); + } + } + + /** + * Sets the inheritance type used by the class and it's subclasses. + * + * @param integer $type + */ + public function setInheritanceType($type) + { + if ( ! $this->_isInheritanceType($type)) { + throw MappingException::invalidInheritanceType($this->name, $type); + } + $this->inheritanceType = $type; + } + + /** + * Checks whether a mapped field is inherited from an entity superclass. + * + * @return boolean TRUE if the field is inherited, FALSE otherwise. + */ + public function isInheritedField($fieldName) + { + return isset($this->fieldMappings[$fieldName]['inherited']); + } + + /** + * Checks whether a mapped association field is inherited from a superclass. + * + * @param string $fieldName + * @return boolean TRUE if the field is inherited, FALSE otherwise. + */ + public function isInheritedAssociation($fieldName) + { + return isset($this->associationMappings[$fieldName]['inherited']); + } + + /** + * Sets the name of the primary table the class is mapped to. + * + * @param string $tableName The table name. + * @deprecated Use {@link setPrimaryTable}. + */ + public function setTableName($tableName) + { + $this->table['name'] = $tableName; + } + + /** + * Sets the primary table definition. The provided array supports the + * following structure: + * + * name => (optional, defaults to class name) + * indexes => array of indexes (optional) + * uniqueConstraints => array of constraints (optional) + * + * If a key is omitted, the current value is kept. + * + * @param array $table The table description. + */ + public function setPrimaryTable(array $table) + { + if (isset($table['name'])) { + if ($table['name'][0] == '`') { + $this->table['name'] = trim($table['name'], '`'); + $this->table['quoted'] = true; + } else { + $this->table['name'] = $table['name']; + } + } + if (isset($table['indexes'])) { + $this->table['indexes'] = $table['indexes']; + } + if (isset($table['uniqueConstraints'])) { + $this->table['uniqueConstraints'] = $table['uniqueConstraints']; + } + } + + /** + * Checks whether the given type identifies an inheritance type. + * + * @param integer $type + * @return boolean TRUE if the given type identifies an inheritance type, FALSe otherwise. + */ + private function _isInheritanceType($type) + { + return $type == self::INHERITANCE_TYPE_NONE || + $type == self::INHERITANCE_TYPE_SINGLE_TABLE || + $type == self::INHERITANCE_TYPE_JOINED || + $type == self::INHERITANCE_TYPE_TABLE_PER_CLASS; + } + + /** + * Adds a mapped field to the class. + * + * @param array $mapping The field mapping. + */ + public function mapField(array $mapping) + { + $this->_validateAndCompleteFieldMapping($mapping); + if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) { + throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']); + } + $this->fieldMappings[$mapping['fieldName']] = $mapping; + } + + /** + * INTERNAL: + * Adds an association mapping without completing/validating it. + * This is mainly used to add inherited association mappings to derived classes. + * + * @param AssociationMapping $mapping + * @param string $owningClassName The name of the class that defined this mapping. + */ + public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/) + { + if (isset($this->associationMappings[$mapping['fieldName']])) { + throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']); + } + $this->associationMappings[$mapping['fieldName']] = $mapping; + } + + /** + * INTERNAL: + * Adds a field mapping without completing/validating it. + * This is mainly used to add inherited field mappings to derived classes. + * + * @param array $mapping + * @todo Rename: addInheritedFieldMapping + */ + public function addInheritedFieldMapping(array $fieldMapping) + { + $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping; + $this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName']; + $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName']; + } + + /** + * Adds a one-to-one mapping. + * + * @param array $mapping The mapping. + */ + public function mapOneToOne(array $mapping) + { + $mapping['type'] = self::ONE_TO_ONE; + $mapping = $this->_validateAndCompleteOneToOneMapping($mapping); + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a one-to-many mapping. + * + * @param array $mapping The mapping. + */ + public function mapOneToMany(array $mapping) + { + $mapping['type'] = self::ONE_TO_MANY; + $mapping = $this->_validateAndCompleteOneToManyMapping($mapping); + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a many-to-one mapping. + * + * @param array $mapping The mapping. + */ + public function mapManyToOne(array $mapping) + { + $mapping['type'] = self::MANY_TO_ONE; + // A many-to-one mapping is essentially a one-one backreference + $mapping = $this->_validateAndCompleteOneToOneMapping($mapping); + $this->_storeAssociationMapping($mapping); + } + + /** + * Adds a many-to-many mapping. + * + * @param array $mapping The mapping. + */ + public function mapManyToMany(array $mapping) + { + $mapping['type'] = self::MANY_TO_MANY; + $mapping = $this->_validateAndCompleteManyToManyMapping($mapping); + $this->_storeAssociationMapping($mapping); + } + + /** + * Stores the association mapping. + * + * @param AssociationMapping $assocMapping + */ + protected function _storeAssociationMapping(array $assocMapping) + { + $sourceFieldName = $assocMapping['fieldName']; + if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) { + throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName); + } + $this->associationMappings[$sourceFieldName] = $assocMapping; + } + + /** + * Registers a custom repository class for the entity class. + * + * @param string $mapperClassName The class name of the custom mapper. + */ + public function setCustomRepositoryClass($repositoryClassName) + { + $this->customRepositoryClassName = $repositoryClassName; + } + + /** + * Dispatches the lifecycle event of the given entity to the registered + * lifecycle callbacks and lifecycle listeners. + * + * @param string $event The lifecycle event. + * @param Entity $entity The Entity on which the event occured. + */ + public function invokeLifecycleCallbacks($lifecycleEvent, $entity) + { + foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) { + $entity->$callback(); + } + } + + /** + * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event. + * + * @param string $lifecycleEvent + * @return boolean + */ + public function hasLifecycleCallbacks($lifecycleEvent) + { + return isset($this->lifecycleCallbacks[$lifecycleEvent]); + } + + /** + * Gets the registered lifecycle callbacks for an event. + * + * @param string $event + * @return array + */ + public function getLifecycleCallbacks($event) + { + return isset($this->lifecycleCallbacks[$event]) ? $this->lifecycleCallbacks[$event] : array(); + } + + /** + * Adds a lifecycle callback for entities of this class. + * + * Note: If the same callback is registered more than once, the old one + * will be overridden. + * + * @param string $callback + * @param string $event + */ + public function addLifecycleCallback($callback, $event) + { + $this->lifecycleCallbacks[$event][] = $callback; + } + + /** + * Sets the lifecycle callbacks for entities of this class. + * Any previously registered callbacks are overwritten. + * + * @param array $callbacks + */ + public function setLifecycleCallbacks(array $callbacks) + { + $this->lifecycleCallbacks = $callbacks; + } + + /** + * Sets the discriminator column definition. + * + * @param array $columnDef + * @see getDiscriminatorColumn() + */ + public function setDiscriminatorColumn($columnDef) + { + if ($columnDef !== null) { + if (isset($this->fieldNames[$columnDef['name']])) { + throw MappingException::duplicateColumnName($this->name, $columnDef['name']); + } + + if ( ! isset($columnDef['name'])) { + throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name, $columnDef); + } + if ( ! isset($columnDef['fieldName'])) { + $columnDef['fieldName'] = $columnDef['name']; + } + if ( ! isset($columnDef['type'])) { + $columnDef['type'] = "string"; + } + if (in_array($columnDef['type'], array("boolean", "array", "object", "datetime", "time", "date"))) { + throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']); + } + + $this->discriminatorColumn = $columnDef; + } + } + + /** + * Sets the discriminator values used by this class. + * Used for JOINED and SINGLE_TABLE inheritance mapping strategies. + * + * @param array $map + */ + public function setDiscriminatorMap(array $map) + { + foreach ($map as $value => $className) { + if (strpos($className, '\\') === false && strlen($this->namespace)) { + $className = $this->namespace . '\\' . $className; + } + $this->discriminatorMap[$value] = $className; + if ($this->name == $className) { + $this->discriminatorValue = $value; + } else { + if ( ! class_exists($className)) { + throw MappingException::invalidClassInDiscriminatorMap($className, $this->name); + } + if (is_subclass_of($className, $this->name)) { + $this->subClasses[] = $className; + } + } + } + } + + /** + * Checks whether the class has a mapped association with the given field name. + * + * @param string $fieldName + * @return boolean + */ + public function hasAssociation($fieldName) + { + return isset($this->associationMappings[$fieldName]); + } + + /** + * Checks whether the class has a mapped association for the specified field + * and if yes, checks whether it is a single-valued association (to-one). + * + * @param string $fieldName + * @return boolean TRUE if the association exists and is single-valued, FALSE otherwise. + */ + public function isSingleValuedAssociation($fieldName) + { + return isset($this->associationMappings[$fieldName]) && + ($this->associationMappings[$fieldName]['type'] & self::TO_ONE); + } + + /** + * Checks whether the class has a mapped association for the specified field + * and if yes, checks whether it is a collection-valued association (to-many). + * + * @param string $fieldName + * @return boolean TRUE if the association exists and is collection-valued, FALSE otherwise. + */ + public function isCollectionValuedAssociation($fieldName) + { + return isset($this->associationMappings[$fieldName]) && + ! ($this->associationMappings[$fieldName]['type'] & self::TO_ONE); + } + + /** + * Sets the ID generator used to generate IDs for instances of this class. + * + * @param AbstractIdGenerator $generator + */ + public function setIdGenerator($generator) + { + $this->idGenerator = $generator; + } + + /** + * Sets the definition of the sequence ID generator for this class. + * + * The definition must have the following structure: + * + * array( + * 'sequenceName' => 'name', + * 'allocationSize' => 20, + * 'initialValue' => 1 + * ) + * + * + * @param array $definition + */ + public function setSequenceGeneratorDefinition(array $definition) + { + $this->sequenceGeneratorDefinition = $definition; + } + + /** + * Sets the version field mapping used for versioning. Sets the default + * value to use depending on the column type. + * + * @param array $mapping The version field mapping array + */ + public function setVersionMapping(array &$mapping) + { + $this->isVersioned = true; + $this->versionField = $mapping['fieldName']; + + if ( ! isset($mapping['default'])) { + if ($mapping['type'] == 'integer') { + $mapping['default'] = 1; + } else if ($mapping['type'] == 'datetime') { + $mapping['default'] = 'CURRENT_TIMESTAMP'; + } else { + throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']); + } + } + } + + /** + * Sets whether this class is to be versioned for optimistic locking. + * + * @param boolean $bool + */ + public function setVersioned($bool) + { + $this->isVersioned = $bool; + } + + /** + * Sets the name of the field that is to be used for versioning if this class is + * versioned for optimistic locking. + * + * @param string $versionField + */ + public function setVersionField($versionField) + { + $this->versionField = $versionField; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php b/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php new file mode 100644 index 0000000..457b7cd --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php @@ -0,0 +1,210 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\MappingException; + +/** + * Base driver for file-based metadata drivers. + * + * A file driver operates in a mode where it loads the mapping files of individual + * classes on demand. This requires the user to adhere to the convention of 1 mapping + * file per class and the file names of the mapping files must correspond to the full + * class name, including namespace, with the namespace delimiters '\', replaced by dots '.'. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +abstract class AbstractFileDriver implements Driver +{ + /** + * The paths where to look for mapping files. + * + * @var array + */ + protected $_paths = array(); + + /** + * The file extension of mapping documents. + * + * @var string + */ + protected $_fileExtension; + + /** + * Initializes a new FileDriver that looks in the given path(s) for mapping + * documents and operates in the specified operating mode. + * + * @param string|array $paths One or multiple paths where mapping documents can be found. + */ + public function __construct($paths) + { + $this->addPaths((array) $paths); + } + + /** + * Append lookup paths to metadata driver. + * + * @param array $paths + */ + public function addPaths(array $paths) + { + $this->_paths = array_unique(array_merge($this->_paths, $paths)); + } + + /** + * Retrieve the defined metadata lookup paths. + * + * @return array + */ + public function getPaths() + { + return $this->_paths; + } + + /** + * Get the file extension used to look for mapping files under + * + * @return void + */ + public function getFileExtension() + { + return $this->_fileExtension; + } + + /** + * Set the file extension used to look for mapping files under + * + * @param string $fileExtension The file extension to set + * @return void + */ + public function setFileExtension($fileExtension) + { + $this->_fileExtension = $fileExtension; + } + + /** + * Get the element of schema meta data for the class from the mapping file. + * This will lazily load the mapping file if it is not loaded yet + * + * @return array $element The element of schema meta data + */ + public function getElement($className) + { + $result = $this->_loadMappingFile($this->_findMappingFile($className)); + + return $result[$className]; + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + $fileName = str_replace('\\', '.', $className) . $this->_fileExtension; + + // Check whether file exists + foreach ((array) $this->_paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $fileName)) { + return false; + } + } + + return true; + } + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + public function getAllClassNames() + { + $classes = array(); + + if ($this->_paths) { + foreach ((array) $this->_paths as $path) { + if ( ! is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (($fileName = $file->getBasename($this->_fileExtension)) == $file->getBasename()) { + continue; + } + + // NOTE: All files found here means classes are not transient! + $classes[] = str_replace('.', '\\', $fileName); + } + } + } + + return $classes; + } + + /** + * Finds the mapping file for the class with the given name by searching + * through the configured paths. + * + * @param $className + * @return string The (absolute) file name. + * @throws MappingException + */ + protected function _findMappingFile($className) + { + $fileName = str_replace('\\', '.', $className) . $this->_fileExtension; + + // Check whether file exists + foreach ((array) $this->_paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $fileName)) { + return $path . DIRECTORY_SEPARATOR . $fileName; + } + } + + throw MappingException::mappingFileNotFound($className, $fileName); + } + + /** + * Loads a mapping file with the given name and returns a map + * from class/entity names to their corresponding elements. + * + * @param string $file The mapping file to load. + * @return array + */ + abstract protected function _loadMappingFile($file); +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php new file mode 100644 index 0000000..115d6db --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -0,0 +1,489 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\Common\Cache\ArrayCache, + Doctrine\Common\Annotations\AnnotationReader, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +require __DIR__ . '/DoctrineAnnotations.php'; + +/** + * The AnnotationDriver reads the mapping metadata from docblock annotations. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class AnnotationDriver implements Driver +{ + /** + * The AnnotationReader. + * + * @var AnnotationReader + */ + private $_reader; + + /** + * The paths where to look for mapping files. + * + * @var array + */ + protected $_paths = array(); + + /** + * The file extension of mapping documents. + * + * @var string + */ + protected $_fileExtension = '.php'; + + /** + * @param array + */ + protected $_classNames; + + /** + * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading + * docblock annotations. + * + * @param $reader The AnnotationReader to use. + * @param string|array $paths One or multiple paths where mapping classes can be found. + */ + public function __construct(AnnotationReader $reader, $paths = null) + { + $this->_reader = $reader; + if ($paths) { + $this->addPaths((array) $paths); + } + } + + /** + * Append lookup paths to metadata driver. + * + * @param array $paths + */ + public function addPaths(array $paths) + { + $this->_paths = array_unique(array_merge($this->_paths, $paths)); + } + + /** + * Retrieve the defined metadata lookup paths. + * + * @return array + */ + public function getPaths() + { + return $this->_paths; + } + + /** + * Get the file extension used to look for mapping files under + * + * @return void + */ + public function getFileExtension() + { + return $this->_fileExtension; + } + + /** + * Set the file extension used to look for mapping files under + * + * @param string $fileExtension The file extension to set + * @return void + */ + public function setFileExtension($fileExtension) + { + $this->_fileExtension = $fileExtension; + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $class = $metadata->getReflectionClass(); + + $classAnnotations = $this->_reader->getClassAnnotations($class); + + // Evaluate Entity annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) { + $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity']; + $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) { + $metadata->isMappedSuperclass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate Table annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Table'])) { + $tableAnnot = $classAnnotations['Doctrine\ORM\Mapping\Table']; + $primaryTable = array( + 'name' => $tableAnnot->name, + 'schema' => $tableAnnot->schema + ); + + if ($tableAnnot->indexes !== null) { + foreach ($tableAnnot->indexes as $indexAnnot) { + $primaryTable['indexes'][$indexAnnot->name] = array( + 'columns' => $indexAnnot->columns + ); + } + } + + if ($tableAnnot->uniqueConstraints !== null) { + foreach ($tableAnnot->uniqueConstraints as $uniqueConstraint) { + $primaryTable['uniqueConstraints'][$uniqueConstraint->name] = array( + 'columns' => $uniqueConstraint->columns + ); + } + } + + $metadata->setPrimaryTable($primaryTable); + } + + // Evaluate InheritanceType annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { + $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; + $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAnnot->value)); + + if ($metadata->inheritanceType != \Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate DiscriminatorColumn annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'])) { + $discrColumnAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn']; + $metadata->setDiscriminatorColumn(array( + 'name' => $discrColumnAnnot->name, + 'type' => $discrColumnAnnot->type, + 'length' => $discrColumnAnnot->length + )); + } else { + $metadata->setDiscriminatorColumn(array('name' => 'dtype', 'type' => 'string', 'length' => 255)); + } + + // Evaluate DiscriminatorMap annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'])) { + $discrMapAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap']; + $metadata->setDiscriminatorMap($discrMapAnnot->value); + } + } + } + + + // Evaluate DoctrineChangeTrackingPolicy annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'])) { + $changeTrackingAnnot = $classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy']; + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAnnot->value)); + } + + // Evaluate annotations on properties/fields + foreach ($class->getProperties() as $property) { + if ($metadata->isMappedSuperclass && ! $property->isPrivate() + || + $metadata->isInheritedField($property->name) + || + $metadata->isInheritedAssociation($property->name)) { + continue; + } + + $mapping = array(); + $mapping['fieldName'] = $property->getName(); + + // Check for JoinColummn/JoinColumns annotations + $joinColumns = array(); + + if ($joinColumnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) { + $joinColumns[] = array( + 'name' => $joinColumnAnnot->name, + 'referencedColumnName' => $joinColumnAnnot->referencedColumnName, + 'unique' => $joinColumnAnnot->unique, + 'nullable' => $joinColumnAnnot->nullable, + 'onDelete' => $joinColumnAnnot->onDelete, + 'onUpdate' => $joinColumnAnnot->onUpdate, + 'columnDefinition' => $joinColumnAnnot->columnDefinition, + ); + } else if ($joinColumnsAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) { + foreach ($joinColumnsAnnot->value as $joinColumn) { + $joinColumns[] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate, + 'columnDefinition' => $joinColumn->columnDefinition, + ); + } + } + + // Field can only be annotated with one of: + // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany + if ($columnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Column')) { + if ($columnAnnot->type == null) { + throw MappingException::propertyTypeIsRequired($className, $property->getName()); + } + + $mapping['type'] = $columnAnnot->type; + $mapping['length'] = $columnAnnot->length; + $mapping['precision'] = $columnAnnot->precision; + $mapping['scale'] = $columnAnnot->scale; + $mapping['nullable'] = $columnAnnot->nullable; + $mapping['unique'] = $columnAnnot->unique; + if ($columnAnnot->options) { + $mapping['options'] = $columnAnnot->options; + } + + if (isset($columnAnnot->name)) { + $mapping['columnName'] = $columnAnnot->name; + } + + if (isset($columnAnnot->columnDefinition)) { + $mapping['columnDefinition'] = $columnAnnot->columnDefinition; + } + + if ($idAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) { + $mapping['id'] = true; + } + + if ($generatedValueAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\GeneratedValue')) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAnnot->strategy)); + } + + if ($versionAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Version')) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + + // Check for SequenceGenerator/TableGenerator definition + if ($seqGeneratorAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\SequenceGenerator')) { + $metadata->setSequenceGeneratorDefinition(array( + 'sequenceName' => $seqGeneratorAnnot->sequenceName, + 'allocationSize' => $seqGeneratorAnnot->allocationSize, + 'initialValue' => $seqGeneratorAnnot->initialValue + )); + } else if ($tblGeneratorAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator')) { + throw MappingException::tableIdGeneratorNotImplemented($className); + } + } else if ($oneToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) { + $mapping['targetEntity'] = $oneToOneAnnot->targetEntity; + $mapping['joinColumns'] = $joinColumns; + $mapping['mappedBy'] = $oneToOneAnnot->mappedBy; + $mapping['inversedBy'] = $oneToOneAnnot->inversedBy; + $mapping['cascade'] = $oneToOneAnnot->cascade; + $mapping['orphanRemoval'] = $oneToOneAnnot->orphanRemoval; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToOneAnnot->fetch); + $metadata->mapOneToOne($mapping); + } else if ($oneToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToMany')) { + $mapping['mappedBy'] = $oneToManyAnnot->mappedBy; + $mapping['targetEntity'] = $oneToManyAnnot->targetEntity; + $mapping['cascade'] = $oneToManyAnnot->cascade; + $mapping['orphanRemoval'] = $oneToManyAnnot->orphanRemoval; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToManyAnnot->fetch); + + if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) { + $mapping['orderBy'] = $orderByAnnot->value; + } + + $metadata->mapOneToMany($mapping); + } else if ($manyToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToOne')) { + $mapping['joinColumns'] = $joinColumns; + $mapping['cascade'] = $manyToOneAnnot->cascade; + $mapping['inversedBy'] = $manyToOneAnnot->inversedBy; + $mapping['targetEntity'] = $manyToOneAnnot->targetEntity; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToOneAnnot->fetch); + $metadata->mapManyToOne($mapping); + } else if ($manyToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) { + $joinTable = array(); + + if ($joinTableAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) { + $joinTable = array( + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema + ); + + foreach ($joinTableAnnot->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate, + 'columnDefinition' => $joinColumn->columnDefinition, + ); + } + + foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = array( + 'name' => $joinColumn->name, + 'referencedColumnName' => $joinColumn->referencedColumnName, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'onUpdate' => $joinColumn->onUpdate, + 'columnDefinition' => $joinColumn->columnDefinition, + ); + } + } + + $mapping['joinTable'] = $joinTable; + $mapping['targetEntity'] = $manyToManyAnnot->targetEntity; + $mapping['mappedBy'] = $manyToManyAnnot->mappedBy; + $mapping['inversedBy'] = $manyToManyAnnot->inversedBy; + $mapping['cascade'] = $manyToManyAnnot->cascade; + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToManyAnnot->fetch); + + if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) { + $mapping['orderBy'] = $orderByAnnot->value; + } + + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate @HasLifecycleCallbacks annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\HasLifecycleCallbacks'])) { + foreach ($class->getMethods() as $method) { + if ($method->isPublic()) { + $annotations = $this->_reader->getMethodAnnotations($method); + + if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove); + } + + if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) { + $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad); + } + } + } + } + } + + /** + * Whether the class with the specified name is transient. Only non-transient + * classes, that is entities and mapped superclasses, should have their metadata loaded. + * A class is non-transient if it is annotated with either @Entity or + * @MappedSuperclass in the class doc block. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + $classAnnotations = $this->_reader->getClassAnnotations(new \ReflectionClass($className)); + + return ! isset($classAnnotations['Doctrine\ORM\Mapping\Entity']) && + ! isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass']); + } + + /** + * {@inheritDoc} + */ + public function getAllClassNames() + { + if ($this->_classNames !== null) { + return $this->_classNames; + } + + if (!$this->_paths) { + throw MappingException::pathRequired(); + } + + $classes = array(); + $includedFiles = array(); + + foreach ($this->_paths as $path) { + if ( ! is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (($fileName = $file->getBasename($this->_fileExtension)) == $file->getBasename()) { + continue; + } + + $sourceFile = realpath($file->getPathName()); + require_once $sourceFile; + $includedFiles[] = $sourceFile; + } + } + + $declared = get_declared_classes(); + + foreach ($declared as $className) { + $rc = new \ReflectionClass($className); + $sourceFile = $rc->getFileName(); + if (in_array($sourceFile, $includedFiles) && ! $this->isTransient($className)) { + $classes[] = $className; + } + } + + $this->_classNames = $classes; + + return $classes; + } + + /** + * Factory method for the Annotation Driver + * + * @param array|string $paths + * @param AnnotationReader $reader + * @return AnnotationDriver + */ + static public function create($paths = array(), AnnotationReader $reader = null) + { + if ($reader == null) { + $reader = new AnnotationReader(); + $reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\'); + } + return new self($reader, $paths); + } +} diff --git a/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php new file mode 100644 index 0000000..c6c4547 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -0,0 +1,276 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\Common\Cache\ArrayCache, + Doctrine\Common\Annotations\AnnotationReader, + Doctrine\DBAL\Schema\AbstractSchemaManager, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException, + Doctrine\Common\Util\Inflector; + +/** + * The DatabaseDriver reverse engineers the mapping metadata from a database. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Benjamin Eberlei + */ +class DatabaseDriver implements Driver +{ + /** + * @var AbstractSchemaManager + */ + private $_sm; + + /** + * @var array + */ + private $tables = null; + + private $classToTableNames = array(); + + /** + * @var array + */ + private $manyToManyTables = array(); + + /** + * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading + * docblock annotations. + * + * @param AnnotationReader $reader The AnnotationReader to use. + */ + public function __construct(AbstractSchemaManager $schemaManager) + { + $this->_sm = $schemaManager; + } + + private function reverseEngineerMappingFromDatabase() + { + if ($this->tables !== null) { + return; + } + + foreach ($this->_sm->listTableNames() as $tableName) { + $tables[$tableName] = $this->_sm->listTableDetails($tableName); + } + + $this->tables = array(); + foreach ($tables AS $tableName => $table) { + /* @var $table Table */ + if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $foreignKeys = $table->getForeignKeys(); + } else { + $foreignKeys = array(); + } + + $allForeignKeyColumns = array(); + foreach ($foreignKeys AS $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + $pkColumns = $table->getPrimaryKey()->getColumns(); + sort($pkColumns); + sort($allForeignKeyColumns); + + if ($pkColumns == $allForeignKeyColumns) { + if (count($table->getForeignKeys()) > 2) { + throw new \InvalidArgumentException("ManyToMany table '" . $tableName . "' with more or less than two foreign keys are not supported by the Database Reverese Engineering Driver."); + } + + $this->manyToManyTables[$tableName] = $table; + } else { + // lower-casing is necessary because of Oracle Uppercase Tablenames, + // assumption is lower-case + underscore separated. + $className = Inflector::classify(strtolower($tableName)); + $this->tables[$tableName] = $table; + $this->classToTableNames[$className] = $tableName; + } + } + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $this->reverseEngineerMappingFromDatabase(); + + if (!isset($this->classToTableNames[$className])) { + throw new \InvalidArgumentException("Unknown class " . $className); + } + + $tableName = $this->classToTableNames[$className]; + + $metadata->name = $className; + $metadata->table['name'] = $tableName; + + $columns = $this->tables[$tableName]->getColumns(); + $indexes = $this->tables[$tableName]->getIndexes(); + + if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { + $foreignKeys = $this->tables[$tableName]->getForeignKeys(); + } else { + $foreignKeys = array(); + } + + $allForeignKeyColumns = array(); + foreach ($foreignKeys AS $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + $ids = array(); + $fieldMappings = array(); + foreach ($columns as $column) { + $fieldMapping = array(); + if (isset($indexes['primary']) && in_array($column->getName(), $indexes['primary']->getColumns())) { + $fieldMapping['id'] = true; + } else if (in_array($column->getName(), $allForeignKeyColumns)) { + continue; + } + + $fieldMapping['fieldName'] = Inflector::camelize(strtolower($column->getName())); + $fieldMapping['columnName'] = $column->getName(); + $fieldMapping['type'] = strtolower((string) $column->getType()); + + if ($column->getType() instanceof \Doctrine\DBAL\Types\StringType) { + $fieldMapping['length'] = $column->getLength(); + $fieldMapping['fixed'] = $column->getFixed(); + } else if ($column->getType() instanceof \Doctrine\DBAL\Types\IntegerType) { + $fieldMapping['unsigned'] = $column->getUnsigned(); + } + $fieldMapping['nullable'] = $column->getNotNull() ? false : true; + + if (isset($fieldMapping['id'])) { + $ids[] = $fieldMapping; + } else { + $fieldMappings[] = $fieldMapping; + } + } + + if ($ids) { + if (count($ids) == 1) { + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + } + + foreach ($ids as $id) { + $metadata->mapField($id); + } + } + + foreach ($fieldMappings as $fieldMapping) { + $metadata->mapField($fieldMapping); + } + + foreach ($this->manyToManyTables AS $manyTable) { + foreach ($manyTable->getForeignKeys() AS $foreignKey) { + if (strtolower($tableName) == strtolower($foreignKey->getForeignTableName())) { + $myFk = $foreignKey; + foreach ($manyTable->getForeignKeys() AS $foreignKey) { + if ($foreignKey != $myFk) { + $otherFk = $foreignKey; + break; + } + } + + $localColumn = current($myFk->getColumns()); + $associationMapping = array(); + $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower(current($otherFk->getColumns())))); + $associationMapping['targetEntity'] = Inflector::classify(strtolower($otherFk->getForeignTableName())); + if (current($manyTable->getColumns())->getName() == $localColumn) { + $associationMapping['inversedBy'] = Inflector::camelize(str_replace('_id', '', strtolower(current($myFk->getColumns())))); + $associationMapping['joinTable'] = array( + 'name' => strtolower($manyTable->getName()), + 'joinColumns' => array(), + 'inverseJoinColumns' => array(), + ); + + $fkCols = $myFk->getForeignColumns(); + $cols = $myFk->getColumns(); + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['joinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + + $fkCols = $otherFk->getForeignColumns(); + $cols = $otherFk->getColumns(); + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['inverseJoinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + } else { + $associationMapping['mappedBy'] = Inflector::camelize(str_replace('_id', '', strtolower(current($myFk->getColumns())))); + } + $metadata->mapManyToMany($associationMapping); + break; + } + } + } + + foreach ($foreignKeys as $foreignKey) { + $foreignTable = $foreignKey->getForeignTableName(); + $cols = $foreignKey->getColumns(); + $fkCols = $foreignKey->getForeignColumns(); + + $localColumn = current($cols); + $associationMapping = array(); + $associationMapping['fieldName'] = Inflector::camelize(str_replace('_id', '', strtolower($localColumn))); + $associationMapping['targetEntity'] = Inflector::classify($foreignTable); + + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + $metadata->mapManyToOne($associationMapping); + } + } + + /** + * {@inheritdoc} + */ + public function isTransient($className) + { + return true; + } + + /** + * Return all the class names supported by this driver. + * + * IMPORTANT: This method must return an array of class not tables names. + * + * @return array + */ + public function getAllClassNames() + { + $this->reverseEngineerMappingFromDatabase(); + + return array_keys($this->classToTableNames); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php new file mode 100644 index 0000000..4356765 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -0,0 +1,137 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use Doctrine\Common\Annotations\Annotation; + +/* Annotations */ + +final class Entity extends Annotation { + public $repositoryClass; +} +final class MappedSuperclass extends Annotation {} +final class InheritanceType extends Annotation {} +final class DiscriminatorColumn extends Annotation { + public $name; + public $fieldName; // field name used in non-object hydration (array/scalar) + public $type; + public $length; +} +final class DiscriminatorMap extends Annotation {} +final class Id extends Annotation {} +final class GeneratedValue extends Annotation { + public $strategy = 'AUTO'; +} +final class Version extends Annotation {} +final class JoinColumn extends Annotation { + public $name; + public $fieldName; // field name used in non-object hydration (array/scalar) + public $referencedColumnName = 'id'; + public $unique = false; + public $nullable = true; + public $onDelete; + public $onUpdate; + public $columnDefinition; +} +final class JoinColumns extends Annotation {} +final class Column extends Annotation { + public $type = 'string'; + public $length; + // The precision for a decimal (exact numeric) column (Applies only for decimal column) + public $precision = 0; + // The scale for a decimal (exact numeric) column (Applies only for decimal column) + public $scale = 0; + public $unique = false; + public $nullable = false; + public $name; + public $options = array(); + public $columnDefinition; +} +final class OneToOne extends Annotation { + public $targetEntity; + public $mappedBy; + public $inversedBy; + public $cascade; + public $fetch = 'LAZY'; + public $orphanRemoval = false; +} +final class OneToMany extends Annotation { + public $mappedBy; + public $targetEntity; + public $cascade; + public $fetch = 'LAZY'; + public $orphanRemoval = false; +} +final class ManyToOne extends Annotation { + public $targetEntity; + public $cascade; + public $fetch = 'LAZY'; + public $inversedBy; +} +final class ManyToMany extends Annotation { + public $targetEntity; + public $mappedBy; + public $inversedBy; + public $cascade; + public $fetch = 'LAZY'; +} +final class ElementCollection extends Annotation { + public $tableName; +} +final class Table extends Annotation { + public $name; + public $schema; + public $indexes; + public $uniqueConstraints; +} +final class UniqueConstraint extends Annotation { + public $name; + public $columns; +} +final class Index extends Annotation { + public $name; + public $columns; +} +final class JoinTable extends Annotation { + public $name; + public $schema; + public $joinColumns = array(); + public $inverseJoinColumns = array(); +} +final class SequenceGenerator extends Annotation { + public $sequenceName; + public $allocationSize = 1; + public $initialValue = 1; +} +final class ChangeTrackingPolicy extends Annotation {} +final class OrderBy extends Annotation {} + +/* Annotations for lifecycle callbacks */ +final class HasLifecycleCallbacks extends Annotation {} +final class PrePersist extends Annotation {} +final class PostPersist extends Annotation {} +final class PreUpdate extends Annotation {} +final class PostUpdate extends Annotation {} +final class PreRemove extends Annotation {} +final class PostRemove extends Annotation {} +final class PostLoad extends Annotation {} + diff --git a/Doctrine/ORM/Mapping/Driver/Driver.php b/Doctrine/ORM/Mapping/Driver/Driver.php new file mode 100644 index 0000000..b6cfe36 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/Driver.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * Contract for metadata drivers. + * + * @since 2.0 + * @author Jonathan H. Wage + * @todo Rename: MetadataDriver or MappingDriver + */ +interface Driver +{ + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadataInfo $metadata + */ + function loadMetadataForClass($className, ClassMetadataInfo $metadata); + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + function getAllClassNames(); + + /** + * Whether the class with the specified name should have its metadata loaded. + * This is only the case if it is either mapped as an Entity or a + * MappedSuperclass. + * + * @param string $className + * @return boolean + */ + function isTransient($className); +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/DriverChain.php b/Doctrine/ORM/Mapping/Driver/DriverChain.php new file mode 100644 index 0000000..d97a61e --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/DriverChain.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\Driver\Driver, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +/** + * The DriverChain allows you to add multiple other mapping drivers for + * certain namespaces + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + * @todo Rename: MappingDriverChain or MetadataDriverChain + */ +class DriverChain implements Driver +{ + /** + * @var array + */ + private $_drivers = array(); + + /** + * Add a nested driver. + * + * @param Driver $nestedDriver + * @param string $namespace + */ + public function addDriver(Driver $nestedDriver, $namespace) + { + $this->_drivers[$namespace] = $nestedDriver; + } + + /** + * Get the array of nested drivers. + * + * @return array $drivers + */ + public function getDrivers() + { + return $this->_drivers; + } + + /** + * Loads the metadata for the specified class into the provided container. + * + * @param string $className + * @param ClassMetadataInfo $metadata + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + foreach ($this->_drivers as $namespace => $driver) { + if (strpos($className, $namespace) === 0) { + $driver->loadMetadataForClass($className, $metadata); + return; + } + } + + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + /** + * Gets the names of all mapped classes known to this driver. + * + * @return array The names of all mapped classes known to this driver. + */ + public function getAllClassNames() + { + $classNames = array(); + foreach ($this->_drivers AS $driver) { + $classNames = array_merge($classNames, $driver->getAllClassNames()); + } + return $classNames; + } + + /** + * Whether the class with the specified name should have its metadata loaded. + * + * This is only the case for non-transient classes either mapped as an Entity or MappedSuperclass. + * + * @param string $className + * @return boolean + */ + public function isTransient($className) + { + foreach ($this->_drivers AS $namespace => $driver) { + if (strpos($className, $namespace) === 0) { + return $driver->isTransient($className); + } + } + + // class isTransient, i.e. not an entity or mapped superclass + return true; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/PHPDriver.php b/Doctrine/ORM/Mapping/Driver/PHPDriver.php new file mode 100644 index 0000000..ff86597 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/PHPDriver.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\Common\Cache\ArrayCache, + Doctrine\Common\Annotations\AnnotationReader, + Doctrine\DBAL\Schema\AbstractSchemaManager, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException, + Doctrine\Common\Util\Inflector, + Doctrine\ORM\Mapping\Driver\AbstractFileDriver; + +/** + * The PHPDriver includes php files which just populate ClassMetadataInfo + * instances with plain php code + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + * @todo Rename: PHPDriver + */ +class PHPDriver extends AbstractFileDriver +{ + /** + * {@inheritdoc} + */ + protected $_fileExtension = '.php'; + protected $_metadata; + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $this->_metadata = $metadata; + $this->_loadMappingFile($this->_findMappingFile($className)); + } + + /** + * {@inheritdoc} + */ + protected function _loadMappingFile($file) + { + $metadata = $this->_metadata; + include $file; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/StaticPHPDriver.php b/Doctrine/ORM/Mapping/Driver/StaticPHPDriver.php new file mode 100644 index 0000000..d89b1ed --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/StaticPHPDriver.php @@ -0,0 +1,122 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +/** + * The StaticPHPDriver calls a static loadMetadata() method on your entity + * classes where you can manually populate the ClassMetadata instance. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class StaticPHPDriver implements Driver +{ + private $_paths = array(); + + public function __construct($paths) + { + $this->addPaths((array) $paths); + } + + public function addPaths(array $paths) + { + $this->_paths = array_unique(array_merge($this->_paths, $paths)); + } + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + call_user_func_array(array($className, 'loadMetadata'), array($metadata)); + } + + /** + * {@inheritDoc} + * @todo Same code exists in AnnotationDriver, should we re-use it somehow or not worry about it? + */ + public function getAllClassNames() + { + if ($this->_classNames !== null) { + return $this->_classNames; + } + + if (!$this->_paths) { + throw MappingException::pathRequired(); + } + + $classes = array(); + $includedFiles = array(); + + foreach ($this->_paths as $path) { + if ( ! is_dir($path)) { + throw MappingException::fileMappingDriversRequireConfiguredDirectoryPath($path); + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($iterator as $file) { + if (($fileName = $file->getBasename($this->_fileExtension)) == $file->getBasename()) { + continue; + } + + $sourceFile = realpath($file->getPathName()); + require_once $sourceFile; + $includedFiles[] = $sourceFile; + } + } + + $declared = get_declared_classes(); + + foreach ($declared as $className) { + $rc = new \ReflectionClass($className); + $sourceFile = $rc->getFileName(); + if (in_array($sourceFile, $includedFiles) && ! $this->isTransient($className)) { + $classes[] = $className; + } + } + + $this->_classNames = $classes; + + return $classes; + } + + /** + * {@inheritdoc} + */ + public function isTransient($className) + { + return method_exists($className, 'loadMetadata') ? false : true; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/Doctrine/ORM/Mapping/Driver/XmlDriver.php new file mode 100644 index 0000000..91892e2 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -0,0 +1,495 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use SimpleXMLElement, + Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +/** + * XmlDriver is a metadata driver that enables mapping through XML files. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class XmlDriver extends AbstractFileDriver +{ + /** + * {@inheritdoc} + */ + protected $_fileExtension = '.dcm.xml'; + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $xmlRoot = $this->getElement($className); + + if ($xmlRoot->getName() == 'entity') { + $metadata->setCustomRepositoryClass( + isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null + ); + } else if ($xmlRoot->getName() == 'mapped-superclass') { + $metadata->isMappedSuperclass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate attributes + $table = array(); + if (isset($xmlRoot['table'])) { + $table['name'] = (string)$xmlRoot['table']; + } + + $metadata->setPrimaryTable($table); + + /* not implemented specially anyway. use table = schema.table + if (isset($xmlRoot['schema'])) { + $metadata->table['schema'] = (string)$xmlRoot['schema']; + }*/ + + if (isset($xmlRoot['inheritance-type'])) { + $inheritanceType = (string)$xmlRoot['inheritance-type']; + $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType)); + + if ($metadata->inheritanceType != \Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate + if (isset($xmlRoot->{'discriminator-column'})) { + $discrColumn = $xmlRoot->{'discriminator-column'}; + $metadata->setDiscriminatorColumn(array( + 'name' => (string)$discrColumn['name'], + 'type' => (string)$discrColumn['type'], + 'length' => (string)$discrColumn['length'] + )); + } else { + $metadata->setDiscriminatorColumn(array('name' => 'dtype', 'type' => 'string', 'length' => 255)); + } + + // Evaluate + if (isset($xmlRoot->{'discriminator-map'})) { + $map = array(); + foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} AS $discrMapElement) { + $map[(string)$discrMapElement['value']] = (string)$discrMapElement['class']; + } + $metadata->setDiscriminatorMap($map); + } + } + } + + + // Evaluate + if (isset($xmlRoot['change-tracking-policy'])) { + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' + . strtoupper((string)$xmlRoot['change-tracking-policy']))); + } + + // Evaluate + if (isset($xmlRoot->indexes)) { + $metadata->table['indexes'] = array(); + foreach ($xmlRoot->indexes->index as $index) { + $columns = explode(',', (string)$index['columns']); + + if (isset($index['name'])) { + $metadata->table['indexes'][(string)$index['name']] = array( + 'columns' => $columns + ); + } else { + $metadata->table['indexes'][] = array( + 'columns' => $columns + ); + } + } + } + + // Evaluate + if (isset($xmlRoot->{'unique-constraints'})) { + $metadata->table['uniqueConstraints'] = array(); + foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $unique) { + $columns = explode(',', (string)$unique['columns']); + + if (isset($unique['name'])) { + $metadata->table['uniqueConstraints'][(string)$unique['name']] = array( + 'columns' => $columns + ); + } else { + $metadata->table['uniqueConstraints'][] = array( + 'columns' => $columns + ); + } + } + } + + // Evaluate mappings + if (isset($xmlRoot->field)) { + foreach ($xmlRoot->field as $fieldMapping) { + $mapping = array( + 'fieldName' => (string)$fieldMapping['name'], + 'type' => (string)$fieldMapping['type'] + ); + + if (isset($fieldMapping['column'])) { + $mapping['columnName'] = (string)$fieldMapping['column']; + } + + if (isset($fieldMapping['length'])) { + $mapping['length'] = (int)$fieldMapping['length']; + } + + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = (int)$fieldMapping['precision']; + } + + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = (int)$fieldMapping['scale']; + } + + if (isset($fieldMapping['unique'])) { + $mapping['unique'] = ((string)$fieldMapping['unique'] == "false") ? false : true; + } + + if (isset($fieldMapping['options'])) { + $mapping['options'] = (array)$fieldMapping['options']; + } + + if (isset($fieldMapping['nullable'])) { + $mapping['nullable'] = ((string)$fieldMapping['nullable'] == "false") ? false : true; + } + + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $metadata->setVersionMapping($mapping); + } + + if (isset($fieldMapping['column-definition'])) { + $mapping['columnDefinition'] = (string)$fieldMapping['column-definition']; + } + + $metadata->mapField($mapping); + } + } + + // Evaluate mappings + foreach ($xmlRoot->id as $idElement) { + $mapping = array( + 'id' => true, + 'fieldName' => (string)$idElement['name'], + 'type' => (string)$idElement['type'] + ); + + if (isset($idElement['column'])) { + $mapping['columnName'] = (string)$idElement['column']; + } + + $metadata->mapField($mapping); + + if (isset($idElement->generator)) { + $strategy = isset($idElement->generator['strategy']) ? + (string)$idElement->generator['strategy'] : 'AUTO'; + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . $strategy)); + } + + // Check for SequenceGenerator/TableGenerator definition + if (isset($idElement->{'sequence-generator'})) { + $seqGenerator = $idElement->{'sequence-generator'}; + $metadata->setSequenceGeneratorDefinition(array( + 'sequenceName' => (string)$seqGenerator['sequence-name'], + 'allocationSize' => (string)$seqGenerator['allocation-size'], + 'initialValue' => (string)$seqGenerator['initial-value'] + )); + } else if (isset($idElement->{'table-generator'})) { + throw MappingException::tableIdGeneratorNotImplemented($className); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-one'})) { + foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) { + $mapping = array( + 'fieldName' => (string)$oneToOneElement['field'], + 'targetEntity' => (string)$oneToOneElement['target-entity'] + ); + + if (isset($oneToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$oneToOneElement['fetch']); + } + + if (isset($oneToOneElement['mapped-by'])) { + $mapping['mappedBy'] = (string)$oneToOneElement['mapped-by']; + } else { + if (isset($oneToOneElement['inversed-by'])) { + $mapping['inversedBy'] = (string)$oneToOneElement['inversed-by']; + } + $joinColumns = array(); + + if (isset($oneToOneElement->{'join-column'})) { + $joinColumns[] = $this->_getJoinColumnMapping($oneToOneElement->{'join-column'}); + } else if (isset($oneToOneElement->{'join-columns'})) { + foreach ($oneToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + } + + if (isset($oneToOneElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToOneElement->cascade); + } + + if (isset($oneToOneElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$oneToOneElement->{'orphan-removal'}; + } + + $metadata->mapOneToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'one-to-many'})) { + foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) { + $mapping = array( + 'fieldName' => (string)$oneToManyElement['field'], + 'targetEntity' => (string)$oneToManyElement['target-entity'], + 'mappedBy' => (string)$oneToManyElement['mapped-by'] + ); + + if (isset($oneToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$oneToManyElement['fetch']); + } + + if (isset($oneToManyElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($oneToManyElement->cascade); + } + + if (isset($oneToManyElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$oneToManyElement->{'orphan-removal'}; + } + + if (isset($oneToManyElement->{'order-by'})) { + $orderBy = array(); + foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} AS $orderByField) { + $orderBy[(string)$orderByField['name']] = (string)$orderByField['direction']; + } + $mapping['orderBy'] = $orderBy; + } + + $metadata->mapOneToMany($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-one'})) { + foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) { + $mapping = array( + 'fieldName' => (string)$manyToOneElement['field'], + 'targetEntity' => (string)$manyToOneElement['target-entity'] + ); + + if (isset($manyToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$manyToOneElement['fetch']); + } + + if (isset($manyToOneElement['inversed-by'])) { + $mapping['inversedBy'] = (string)$manyToOneElement['inversed-by']; + } + + $joinColumns = array(); + + if (isset($manyToOneElement->{'join-column'})) { + $joinColumns[] = $this->_getJoinColumnMapping($manyToOneElement->{'join-column'}); + } else if (isset($manyToOneElement->{'join-columns'})) { + foreach ($manyToOneElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + + if (isset($manyToOneElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($manyToOneElement->cascade); + } + + if (isset($manyToOneElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$manyToOneElement->{'orphan-removal'}; + } + + $metadata->mapManyToOne($mapping); + } + } + + // Evaluate mappings + if (isset($xmlRoot->{'many-to-many'})) { + foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) { + $mapping = array( + 'fieldName' => (string)$manyToManyElement['field'], + 'targetEntity' => (string)$manyToManyElement['target-entity'] + ); + + if (isset($manyToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$manyToManyElement['fetch']); + } + + if (isset($manyToManyElement['mapped-by'])) { + $mapping['mappedBy'] = (string)$manyToManyElement['mapped-by']; + } else if (isset($manyToManyElement->{'join-table'})) { + if (isset($manyToManyElement['inversed-by'])) { + $mapping['inversedBy'] = (string)$manyToManyElement['inversed-by']; + } + + $joinTableElement = $manyToManyElement->{'join-table'}; + $joinTable = array( + 'name' => (string)$joinTableElement['name'] + ); + + if (isset($joinTableElement['schema'])) { + $joinTable['schema'] = (string)$joinTableElement['schema']; + } + + foreach ($joinTableElement->{'join-columns'}->{'join-column'} as $joinColumnElement) { + $joinTable['joinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + + foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} as $joinColumnElement) { + $joinTable['inverseJoinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + + $mapping['joinTable'] = $joinTable; + } + + if (isset($manyToManyElement->cascade)) { + $mapping['cascade'] = $this->_getCascadeMappings($manyToManyElement->cascade); + } + + if (isset($manyToManyElement->{'orphan-removal'})) { + $mapping['orphanRemoval'] = (bool)$manyToManyElement->{'orphan-removal'}; + } + + if (isset($manyToManyElement->{'order-by'})) { + $orderBy = array(); + foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} AS $orderByField) { + $orderBy[(string)$orderByField['name']] = (string)$orderByField['direction']; + } + $mapping['orderBy'] = $orderBy; + } + + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate + if (isset($xmlRoot->{'lifecycle-callbacks'})) { + foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} as $lifecycleCallback) { + $metadata->addLifecycleCallback((string)$lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string)$lifecycleCallback['type'])); + } + } + } + + /** + * Constructs a joinColumn mapping array based on the information + * found in the given SimpleXMLElement. + * + * @param $joinColumnElement The XML element. + * @return array The mapping array. + */ + private function _getJoinColumnMapping(SimpleXMLElement $joinColumnElement) + { + $joinColumn = array( + 'name' => (string)$joinColumnElement['name'], + 'referencedColumnName' => (string)$joinColumnElement['referenced-column-name'] + ); + + if (isset($joinColumnElement['unique'])) { + $joinColumn['unique'] = ((string)$joinColumnElement['unique'] == "false") ? false : true; + } + + if (isset($joinColumnElement['nullable'])) { + $joinColumn['nullable'] = ((string)$joinColumnElement['nullable'] == "false") ? false : true; + } + + if (isset($joinColumnElement['on-delete'])) { + $joinColumn['onDelete'] = (string)$joinColumnElement['on-delete']; + } + + if (isset($joinColumnElement['on-update'])) { + $joinColumn['onUpdate'] = (string)$joinColumnElement['on-update']; + } + + if (isset($joinColumnElement['column-definition'])) { + $joinColumn['columnDefinition'] = (string)$joinColumnElement['column-definition']; + } + + return $joinColumn; + } + + /** + * Gathers a list of cascade options found in the given cascade element. + * + * @param $cascadeElement The cascade element. + * @return array The list of cascade options. + */ + private function _getCascadeMappings($cascadeElement) + { + $cascades = array(); + foreach ($cascadeElement->children() as $action) { + // According to the JPA specifications, XML uses "cascade-persist" + // instead of "persist". Here, both variations + // are supported because both YAML and Annotation use "persist" + // and we want to make sure that this driver doesn't need to know + // anything about the supported cascading actions + $cascades[] = str_replace('cascade-', '', $action->getName()); + } + return $cascades; + } + + /** + * {@inheritdoc} + */ + protected function _loadMappingFile($file) + { + $result = array(); + $xmlElement = simplexml_load_file($file); + + if (isset($xmlElement->entity)) { + foreach ($xmlElement->entity as $entityElement) { + $entityName = (string)$entityElement['name']; + $result[$entityName] = $entityElement; + } + } else if (isset($xmlElement->{'mapped-superclass'})) { + foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) { + $className = (string)$mappedSuperClass['name']; + $result[$className] = $mappedSuperClass; + } + } + + return $result; + } +} diff --git a/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/Doctrine/ORM/Mapping/Driver/YamlDriver.php new file mode 100644 index 0000000..e714c50 --- /dev/null +++ b/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -0,0 +1,455 @@ +. + */ + +namespace Doctrine\ORM\Mapping\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\MappingException; + +/** + * The YamlDriver reads the mapping metadata from yaml schema files. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan H. Wage + * @author Roman Borschel + */ +class YamlDriver extends AbstractFileDriver +{ + /** + * {@inheritdoc} + */ + protected $_fileExtension = '.dcm.yml'; + + /** + * {@inheritdoc} + */ + public function loadMetadataForClass($className, ClassMetadataInfo $metadata) + { + $element = $this->getElement($className); + + if ($element['type'] == 'entity') { + $metadata->setCustomRepositoryClass( + isset($element['repositoryClass']) ? $element['repositoryClass'] : null + ); + } else if ($element['type'] == 'mappedSuperclass') { + $metadata->isMappedSuperclass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate root level properties + $table = array(); + if (isset($element['table'])) { + $table['name'] = $element['table']; + } + $metadata->setPrimaryTable($table); + + /* not implemented specially anyway. use table = schema.table + if (isset($element['schema'])) { + $metadata->table['schema'] = $element['schema']; + }*/ + + if (isset($element['inheritanceType'])) { + $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . strtoupper($element['inheritanceType']))); + + if ($metadata->inheritanceType != \Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate discriminatorColumn + if (isset($element['discriminatorColumn'])) { + $discrColumn = $element['discriminatorColumn']; + $metadata->setDiscriminatorColumn(array( + 'name' => $discrColumn['name'], + 'type' => $discrColumn['type'], + 'length' => $discrColumn['length'] + )); + } else { + $metadata->setDiscriminatorColumn(array('name' => 'dtype', 'type' => 'string', 'length' => 255)); + } + + // Evaluate discriminatorMap + if (isset($element['discriminatorMap'])) { + $metadata->setDiscriminatorMap($element['discriminatorMap']); + } + } + } + + + // Evaluate changeTrackingPolicy + if (isset($element['changeTrackingPolicy'])) { + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' + . strtoupper($element['changeTrackingPolicy']))); + } + + // Evaluate indexes + if (isset($element['indexes'])) { + foreach ($element['indexes'] as $name => $index) { + if ( ! isset($index['name'])) { + $index['name'] = $name; + } + + if (is_string($index['columns'])) { + $columns = explode(',', $index['columns']); + } else { + $columns = $index['columns']; + } + + $metadata->table['indexes'][$index['name']] = array( + 'columns' => $columns + ); + } + } + + // Evaluate uniqueConstraints + if (isset($element['uniqueConstraints'])) { + foreach ($element['uniqueConstraints'] as $name => $unique) { + if ( ! isset($unique['name'])) { + $unique['name'] = $name; + } + + if (is_string($unique['columns'])) { + $columns = explode(',', $unique['columns']); + } else { + $columns = $unique['columns']; + } + + $metadata->table['uniqueConstraints'][$unique['name']] = array( + 'columns' => $columns + ); + } + } + + if (isset($element['id'])) { + // Evaluate identifier settings + foreach ($element['id'] as $name => $idElement) { + if (!isset($idElement['type'])) { + throw MappingException::propertyTypeIsRequired($className, $name); + } + + $mapping = array( + 'id' => true, + 'fieldName' => $name, + 'type' => $idElement['type'] + ); + + if (isset($idElement['column'])) { + $mapping['columnName'] = $idElement['column']; + } + + if (isset($idElement['length'])) { + $mapping['length'] = $idElement['length']; + } + + $metadata->mapField($mapping); + + if (isset($idElement['generator'])) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . strtoupper($idElement['generator']['strategy']))); + } + // Check for SequenceGenerator/TableGenerator definition + if (isset($idElement['sequenceGenerator'])) { + $metadata->setSequenceGeneratorDefinition($idElement['sequenceGenerator']); + } else if (isset($idElement['tableGenerator'])) { + throw MappingException::tableIdGeneratorNotImplemented($className); + } + } + } + + // Evaluate fields + if (isset($element['fields'])) { + foreach ($element['fields'] as $name => $fieldMapping) { + if (!isset($fieldMapping['type'])) { + throw MappingException::propertyTypeIsRequired($className, $name); + } + + $e = explode('(', $fieldMapping['type']); + $fieldMapping['type'] = $e[0]; + if (isset($e[1])) { + $fieldMapping['length'] = substr($e[1], 0, strlen($e[1]) - 1); + } + $mapping = array( + 'fieldName' => $name, + 'type' => $fieldMapping['type'] + ); + if (isset($fieldMapping['id'])) { + $mapping['id'] = true; + if (isset($fieldMapping['generator']['strategy'])) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' + . strtoupper($fieldMapping['generator']['strategy']))); + } + } + if (isset($fieldMapping['column'])) { + $mapping['columnName'] = $fieldMapping['column']; + } + if (isset($fieldMapping['length'])) { + $mapping['length'] = $fieldMapping['length']; + } + if (isset($fieldMapping['precision'])) { + $mapping['precision'] = $fieldMapping['precision']; + } + if (isset($fieldMapping['scale'])) { + $mapping['scale'] = $fieldMapping['scale']; + } + if (isset($fieldMapping['unique'])) { + $mapping['unique'] = (bool)$fieldMapping['unique']; + } + if (isset($fieldMapping['options'])) { + $mapping['options'] = $fieldMapping['options']; + } + if (isset($fieldMapping['nullable'])) { + $mapping['nullable'] = $fieldMapping['nullable']; + } + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $metadata->setVersionMapping($mapping); + } + if (isset($fieldMapping['columnDefinition'])) { + $mapping['columnDefinition'] = $fieldMapping['columnDefinition']; + } + + $metadata->mapField($mapping); + } + } + + // Evaluate oneToOne relationships + if (isset($element['oneToOne'])) { + foreach ($element['oneToOne'] as $name => $oneToOneElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $oneToOneElement['targetEntity'] + ); + + if (isset($oneToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToOneElement['fetch']); + } + + if (isset($oneToOneElement['mappedBy'])) { + $mapping['mappedBy'] = $oneToOneElement['mappedBy']; + } else { + if (isset($oneToOneElement['inversedBy'])) { + $mapping['inversedBy'] = $oneToOneElement['inversedBy']; + } + + $joinColumns = array(); + + if (isset($oneToOneElement['joinColumn'])) { + $joinColumns[] = $this->_getJoinColumnMapping($oneToOneElement['joinColumn']); + } else if (isset($oneToOneElement['joinColumns'])) { + foreach ($oneToOneElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + } + + if (isset($oneToOneElement['cascade'])) { + $mapping['cascade'] = $oneToOneElement['cascade']; + } + + $metadata->mapOneToOne($mapping); + } + } + + // Evaluate oneToMany relationships + if (isset($element['oneToMany'])) { + foreach ($element['oneToMany'] as $name => $oneToManyElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $oneToManyElement['targetEntity'], + 'mappedBy' => $oneToManyElement['mappedBy'] + ); + + if (isset($oneToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToManyElement['fetch']); + } + + if (isset($oneToManyElement['cascade'])) { + $mapping['cascade'] = $oneToManyElement['cascade']; + } + + if (isset($oneToManyElement['orderBy'])) { + $mapping['orderBy'] = $oneToManyElement['orderBy']; + } + + $metadata->mapOneToMany($mapping); + } + } + + // Evaluate manyToOne relationships + if (isset($element['manyToOne'])) { + foreach ($element['manyToOne'] as $name => $manyToOneElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $manyToOneElement['targetEntity'] + ); + + if (isset($manyToOneElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToOneElement['fetch']); + } + + if (isset($manyToOneElement['inversedBy'])) { + $mapping['inversedBy'] = $manyToOneElement['inversedBy']; + } + + $joinColumns = array(); + + if (isset($manyToOneElement['joinColumn'])) { + $joinColumns[] = $this->_getJoinColumnMapping($manyToOneElement['joinColumn']); + } else if (isset($manyToOneElement['joinColumns'])) { + foreach ($manyToOneElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinColumns[] = $this->_getJoinColumnMapping($joinColumnElement); + } + } + + $mapping['joinColumns'] = $joinColumns; + + if (isset($manyToOneElement['cascade'])) { + $mapping['cascade'] = $manyToOneElement['cascade']; + } + + $metadata->mapManyToOne($mapping); + } + } + + // Evaluate manyToMany relationships + if (isset($element['manyToMany'])) { + foreach ($element['manyToMany'] as $name => $manyToManyElement) { + $mapping = array( + 'fieldName' => $name, + 'targetEntity' => $manyToManyElement['targetEntity'] + ); + + if (isset($manyToManyElement['fetch'])) { + $mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToManyElement['fetch']); + } + + if (isset($manyToManyElement['mappedBy'])) { + $mapping['mappedBy'] = $manyToManyElement['mappedBy']; + } else if (isset($manyToManyElement['joinTable'])) { + if (isset($manyToManyElement['inversedBy'])) { + $mapping['inversedBy'] = $manyToManyElement['inversedBy']; + } + + $joinTableElement = $manyToManyElement['joinTable']; + $joinTable = array( + 'name' => $joinTableElement['name'] + ); + + if (isset($joinTableElement['schema'])) { + $joinTable['schema'] = $joinTableElement['schema']; + } + + foreach ($joinTableElement['joinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinTable['joinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + + foreach ($joinTableElement['inverseJoinColumns'] as $name => $joinColumnElement) { + if (!isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $name; + } + + $joinTable['inverseJoinColumns'][] = $this->_getJoinColumnMapping($joinColumnElement); + } + + $mapping['joinTable'] = $joinTable; + } + + if (isset($manyToManyElement['cascade'])) { + $mapping['cascade'] = $manyToManyElement['cascade']; + } + + if (isset($manyToManyElement['orderBy'])) { + $mapping['orderBy'] = $manyToManyElement['orderBy']; + } + + $metadata->mapManyToMany($mapping); + } + } + + // Evaluate lifeCycleCallbacks + if (isset($element['lifecycleCallbacks'])) { + foreach ($element['lifecycleCallbacks'] as $type => $methods) { + foreach ($methods as $method) { + $metadata->addLifecycleCallback($method, constant('Doctrine\ORM\Events::' . $type)); + } + } + } + } + + /** + * Constructs a joinColumn mapping array based on the information + * found in the given join column element. + * + * @param $joinColumnElement The array join column element + * @return array The mapping array. + */ + private function _getJoinColumnMapping($joinColumnElement) + { + $joinColumn = array( + 'name' => $joinColumnElement['name'], + 'referencedColumnName' => $joinColumnElement['referencedColumnName'] + ); + + if (isset($joinColumnElement['fieldName'])) { + $joinColumn['fieldName'] = (string) $joinColumnElement['fieldName']; + } + + if (isset($joinColumnElement['unique'])) { + $joinColumn['unique'] = (bool) $joinColumnElement['unique']; + } + + if (isset($joinColumnElement['nullable'])) { + $joinColumn['nullable'] = (bool) $joinColumnElement['nullable']; + } + + if (isset($joinColumnElement['onDelete'])) { + $joinColumn['onDelete'] = $joinColumnElement['onDelete']; + } + + if (isset($joinColumnElement['onUpdate'])) { + $joinColumn['onUpdate'] = $joinColumnElement['onUpdate']; + } + + if (isset($joinColumnElement['columnDefinition'])) { + $joinColumn['columnDefinition'] = $joinColumnElement['columnDefinition']; + } + + return $joinColumn; + } + + /** + * {@inheritdoc} + */ + protected function _loadMappingFile($file) + { + return \Symfony\Component\Yaml\Yaml::load($file); + } +} diff --git a/Doctrine/ORM/Mapping/MappingException.php b/Doctrine/ORM/Mapping/MappingException.php new file mode 100644 index 0000000..a7b1c75 --- /dev/null +++ b/Doctrine/ORM/Mapping/MappingException.php @@ -0,0 +1,230 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * A MappingException indicates that something is wrong with the mapping setup. + * + * @since 2.0 + */ +class MappingException extends \Doctrine\ORM\ORMException +{ + public static function pathRequired() + { + return new self("Specifying the paths to your entities is required ". + "in the AnnotationDriver to retrieve all class names."); + } + + public static function identifierRequired($entityName) + { + return new self("No identifier/primary key specified for Entity '$entityName'." + . " Every Entity must have an identifier/primary key."); + } + + public static function invalidInheritanceType($entityName, $type) + { + return new self("The inheritance type '$type' specified for '$entityName' does not exist."); + } + + public static function generatorNotAllowedWithCompositeId() + { + return new self("Id generators can't be used with a composite id."); + } + + public static function missingFieldName() + { + return new self("The association mapping misses the 'fieldName' attribute."); + } + + public static function missingTargetEntity($fieldName) + { + return new self("The association mapping '$fieldName' misses the 'targetEntity' attribute."); + } + + public static function missingSourceEntity($fieldName) + { + return new self("The association mapping '$fieldName' misses the 'sourceEntity' attribute."); + } + + public static function mappingFileNotFound($entityName, $fileName) + { + return new self("No mapping file found named '$fileName' for class '$entityName'."); + } + + public static function mappingNotFound($fieldName) + { + return new self("No mapping found for field '$fieldName'."); + } + + public static function oneToManyRequiresMappedBy($fieldName) + { + return new self("OneToMany mapping on field '$fieldName' requires the 'mappedBy' attribute."); + } + + public static function joinTableRequired($fieldName) + { + return new self("The mapping of field '$fieldName' requires an the 'joinTable' attribute."); + } + + /** + * Called if a required option was not found but is required + * + * @param string $field which field cannot be processed? + * @param string $expectedOption which option is required + * @param string $hint Can optionally be used to supply a tip for common mistakes, + * e.g. "Did you think of the plural s?" + * @return MappingException + */ + static function missingRequiredOption($field, $expectedOption, $hint = '') + { + $message = "The mapping of field '{$field}' is invalid: The option '{$expectedOption}' is required."; + + if ( ! empty($hint)) { + $message .= ' (Hint: ' . $hint . ')'; + } + + return new self($message); + } + + /** + * Generic exception for invalid mappings. + * + * @param string $fieldName + */ + public static function invalidMapping($fieldName) + { + return new self("The mapping of field '$fieldName' is invalid."); + } + + /** + * Exception for reflection exceptions - adds the entity name, + * because there might be long classnames that will be shortened + * within the stacktrace + * + * @param string $entity The entity's name + * @param \ReflectionException $previousException + */ + public static function reflectionFailure($entity, \ReflectionException $previousException) + { + return new self('An error occurred in ' . $entity, 0, $previousException); + } + + public static function joinColumnMustPointToMappedField($className, $joinColumn) + { + return new self('The column ' . $joinColumn . ' must be mapped to a field in class ' + . $className . ' since it is referenced by a join column of another class.'); + } + + public static function classIsNotAValidEntityOrMappedSuperClass($className) + { + return new self('Class '.$className.' is not a valid entity or mapped super class.'); + } + + public static function propertyTypeIsRequired($className, $propertyName) + { + return new self("The attribute 'type' is required for the column description of property ".$className."::\$".$propertyName."."); + } + + public static function tableIdGeneratorNotImplemented($className) + { + return new self("TableIdGenerator is not yet implemented for use with class ".$className); + } + + /** + * + * @param string $entity The entity's name + * @param string $fieldName The name of the field that was already declared + */ + public static function duplicateFieldMapping($entity, $fieldName) { + return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); + } + + public static function duplicateAssociationMapping($entity, $fieldName) { + return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once'); + } + + public static function singleIdNotAllowedOnCompositePrimaryKey($entity) { + return new self('Single id is not allowed on composite primary key in entity '.$entity); + } + + public static function unsupportedOptimisticLockingType($entity, $fieldName, $unsupportedType) { + return new self('Locking type "'.$unsupportedType.'" (specified in "'.$entity.'", field "'.$fieldName.'") ' + .'is not supported by Doctrine.' + ); + } + + public static function fileMappingDriversRequireConfiguredDirectoryPath($path = null) + { + if ( ! empty($path)) { + $path = '[' . $path . ']'; + } + + return new self( + 'File mapping drivers must have a valid directory path, ' . + 'however the given path ' . $path . ' seems to be incorrect!' + ); + } + + /** + * Throws an exception that indicates that a class used in a discriminator map does not exist. + * An example would be an outdated (maybe renamed) classname. + * + * @param string $className The class that could not be found + * @param string $owningClass The class that declares the discriminator map. + * @return self + */ + public static function invalidClassInDiscriminatorMap($className, $owningClass) { + return new self( + "Entity class '$className' used in the discriminator map of class '$owningClass' ". + "does not exist." + ); + } + + public static function missingDiscriminatorMap($className) + { + return new self("Entity class '$className' is using inheritance but no discriminator map was defined."); + } + + public static function missingDiscriminatorColumn($className) + { + return new self("Entity class '$className' is using inheritance but no discriminator column was defined."); + } + + public static function invalidDiscriminatorColumnType($className, $type) + { + return new self("Discriminator column type on entity class '$className' is not allowed to be '$type'. 'string' or 'integer' type variables are suggested!"); + } + + public static function cannotVersionIdField($className, $fieldName) + { + return new self("Setting Id field '$fieldName' as versionale in entity class '$className' is not supported."); + } + + /** + * @param string $className + * @param string $columnName + * @return self + */ + public static function duplicateColumnName($className, $columnName) + { + return new self("Duplicate definition of column '".$columnName."' on entity '".$className."' in a field or discriminator column mapping."); + } + +} \ No newline at end of file diff --git a/Doctrine/ORM/NativeQuery.php b/Doctrine/ORM/NativeQuery.php new file mode 100644 index 0000000..2c0a5ab --- /dev/null +++ b/Doctrine/ORM/NativeQuery.php @@ -0,0 +1,73 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Represents a native SQL query. + * + * @author Roman Borschel + * @since 2.0 + */ +final class NativeQuery extends AbstractQuery +{ + private $_sql; + + /** + * Sets the SQL of the query. + * + * @param string $sql + * @return NativeQuery This query instance. + */ + public function setSQL($sql) + { + $this->_sql = $sql; + return $this; + } + + /** + * Gets the SQL query. + * + * @return mixed The built SQL query or an array of all SQL queries. + * @override + */ + public function getSQL() + { + return $this->_sql; + } + + /** + * {@inheritdoc} + */ + protected function _doExecute() + { + $stmt = $this->_em->getConnection()->prepare($this->_sql); + $params = $this->_params; + foreach ($params as $key => $value) { + if (isset($this->_paramTypes[$key])) { + $stmt->bindValue($key, $value, $this->_paramTypes[$key]); + } else { + $stmt->bindValue($key, $value); + } + } + $stmt->execute(); + + return $stmt; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/NoResultException.php b/Doctrine/ORM/NoResultException.php new file mode 100644 index 0000000..80f08bb --- /dev/null +++ b/Doctrine/ORM/NoResultException.php @@ -0,0 +1,34 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Exception thrown when an ORM query unexpectedly does not return any results. + * + * @author robo + * @since 2.0 + */ +class NoResultException extends ORMException +{ + public function __construct() + { + parent::__construct('No result was found for query although at least one row was expected.'); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/NonUniqueResultException.php b/Doctrine/ORM/NonUniqueResultException.php new file mode 100644 index 0000000..811df75 --- /dev/null +++ b/Doctrine/ORM/NonUniqueResultException.php @@ -0,0 +1,28 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Exception thrown when an ORM query unexpectedly returns more than one result. + * + * @author robo + * @since 2.0 + */ +class NonUniqueResultException extends ORMException {} \ No newline at end of file diff --git a/Doctrine/ORM/ORMException.php b/Doctrine/ORM/ORMException.php new file mode 100644 index 0000000..c84dec4 --- /dev/null +++ b/Doctrine/ORM/ORMException.php @@ -0,0 +1,118 @@ +. + */ + +namespace Doctrine\ORM; + +use Exception; + +/** + * Base exception class for all ORM exceptions. + * + * @author Roman Borschel + * @since 2.0 + */ +class ORMException extends Exception +{ + public static function missingMappingDriverImpl() + { + return new self("It's a requirement to specify a Metadata Driver and pass it ". + "to Doctrine\ORM\Configuration::setMetadataDriverImpl()."); + } + + public static function entityMissingAssignedId($entity) + { + return new self("Entity of type " . get_class($entity) . " is missing an assigned ID."); + } + + public static function unrecognizedField($field) + { + return new self("Unrecognized field: $field"); + } + + public static function invalidFlushMode($mode) + { + return new self("'$mode' is an invalid flush mode."); + } + + public static function entityManagerClosed() + { + return new self("The EntityManager is closed."); + } + + public static function invalidHydrationMode($mode) + { + return new self("'$mode' is an invalid hydration mode."); + } + + public static function mismatchedEventManager() + { + return new self("Cannot use different EventManager instances for EntityManager and Connection."); + } + + public static function findByRequiresParameter($methodName) + { + return new self("You need to pass a parameter to '".$methodName."'"); + } + + public static function invalidFindByCall($entityName, $fieldName, $method) + { + return new self( + "Entity '".$entityName."' has no field '".$fieldName."'. ". + "You can therefore not call '".$method."' on the entities' repository" + ); + } + + public static function invalidFindByInverseAssociation($entityName, $associationFieldName) + { + return new self( + "You cannot search for the association field '".$entityName."#".$associationFieldName."', ". + "because it is the inverse side of an association. Find methods only work on owning side associations." + ); + } + + public static function invalidResultCacheDriver() { + return new self("Invalid result cache driver; it must implement \Doctrine\Common\Cache\Cache."); + } + + public static function notSupported() { + return new self("This behaviour is (currently) not supported by Doctrine 2"); + } + + public static function queryCacheNotConfigured() + { + return new self('Query Cache is not configured.'); + } + + public static function metadataCacheNotConfigured() + { + return new self('Class Metadata Cache is not configured.'); + } + + public static function proxyClassesAlwaysRegenerating() + { + return new self('Proxy Classes are always regenerating.'); + } + + public static function unknownEntityNamespace($entityNamespaceAlias) + { + return new self( + "Unknown Entity namespace alias '$entityNamespaceAlias'." + ); + } +} diff --git a/Doctrine/ORM/OptimisticLockException.php b/Doctrine/ORM/OptimisticLockException.php new file mode 100644 index 0000000..028698c --- /dev/null +++ b/Doctrine/ORM/OptimisticLockException.php @@ -0,0 +1,63 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * An OptimisticLockException is thrown when a version check on an object + * that uses optimistic locking through a version field fails. + * + * @author Roman Borschel + * @author Benjamin Eberlei + * @since 2.0 + */ +class OptimisticLockException extends ORMException +{ + private $entity; + + public function __construct($msg, $entity) + { + $this->entity = $entity; + } + + /** + * Gets the entity that caused the exception. + * + * @return object + */ + public function getEntity() + { + return $this->entity; + } + + public static function lockFailed($entity) + { + return new self("The optimistic lock on an entity failed.", $entity); + } + + public static function lockFailedVersionMissmatch($entity, $expectedLockVersion, $actualLockVersion) + { + return new self("The optimistic lock failed, version " . $expectedLockVersion . " was expected, but is actually ".$actualLockVersion, $entity); + } + + public static function notVersioned($entityName) + { + return new self("Cannot obtain optimistic lock on unversioned entity " . $entityName, null); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/PersistentCollection.php b/Doctrine/ORM/PersistentCollection.php new file mode 100644 index 0000000..bf7c6da --- /dev/null +++ b/Doctrine/ORM/PersistentCollection.php @@ -0,0 +1,681 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\Common\Collections\Collection, + Closure; + +/** + * A PersistentCollection represents a collection of elements that have persistent state. + * + * Collections of entities represent only the associations (links) to those entities. + * That means, if the collection is part of a many-many mapping and you remove + * entities from the collection, only the links in the relation table are removed (on flush). + * Similarly, if you remove entities from a collection that is part of a one-many + * mapping this will only result in the nulling out of the foreign keys on flush. + * + * @since 2.0 + * @author Konsta Vesterinen + * @author Roman Borschel + * @author Giorgio Sironi + * @todo Design for inheritance to allow custom implementations? + */ +final class PersistentCollection implements Collection +{ + /** + * 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 + */ + private $snapshot = array(); + + /** + * The entity that owns this collection. + * + * @var object + */ + private $owner; + + /** + * The association mapping the collection belongs to. + * This is currently either a OneToManyMapping or a ManyToManyMapping. + * + * @var Doctrine\ORM\Mapping\AssociationMapping + */ + private $association; + + /** + * The EntityManager that manages the persistence of the collection. + * + * @var Doctrine\ORM\EntityManager + */ + private $em; + + /** + * The name of the field on the target entities that points to the owner + * of the collection. This is only set if the association is bi-directional. + * + * @var string + */ + private $backRefFieldName; + + /** + * The class descriptor of the collection's entity type. + */ + private $typeClass; + + /** + * Whether the collection is dirty and needs to be synchronized with the database + * when the UnitOfWork that manages its persistent state commits. + * + * @var boolean + */ + private $isDirty = false; + + /** + * Whether the collection has already been initialized. + * + * @var boolean + */ + private $initialized = true; + + /** + * The wrapped Collection instance. + * + * @var Collection + */ + private $coll; + + /** + * Creates a new persistent collection. + * + * @param EntityManager $em The EntityManager the collection will be associated with. + * @param ClassMetadata $class The class descriptor of the entity type of this collection. + * @param array The collection elements. + */ + public function __construct(EntityManager $em, $class, $coll) + { + $this->coll = $coll; + $this->em = $em; + $this->typeClass = $class; + } + + /** + * INTERNAL: + * Sets the collection's owning entity together with the AssociationMapping that + * describes the association between the owner and the elements of the collection. + * + * @param object $entity + * @param AssociationMapping $assoc + */ + public function setOwner($entity, array $assoc) + { + $this->owner = $entity; + $this->association = $assoc; + $this->backRefFieldName = $assoc['inversedBy'] ?: $assoc['mappedBy']; + } + + /** + * INTERNAL: + * Gets the collection owner. + * + * @return object + */ + public function getOwner() + { + return $this->owner; + } + + public function getTypeClass() + { + return $this->typeClass; + } + + /** + * INTERNAL: + * Adds an element to a collection during hydration. This will automatically + * complete bidirectional associations in the case of a one-to-many association. + * + * @param mixed $element The element to add. + */ + public function hydrateAdd($element) + { + $this->coll->add($element); + // If _backRefFieldName is set and its a one-to-many association, + // we need to set the back reference. + if ($this->backRefFieldName && $this->association['type'] == ClassMetadata::ONE_TO_MANY) { + // Set back reference to owner + $this->typeClass->reflFields[$this->backRefFieldName] + ->setValue($element, $this->owner); + $this->em->getUnitOfWork()->setOriginalEntityProperty( + spl_object_hash($element), + $this->backRefFieldName, + $this->owner); + } + } + + /** + * INTERNAL: + * Sets a keyed element in the collection during hydration. + * + * @param mixed $key The key to set. + * $param mixed $value The element to set. + */ + public function hydrateSet($key, $element) + { + $this->coll->set($key, $element); + // If _backRefFieldName is set, then the association is bidirectional + // and we need to set the back reference. + if ($this->backRefFieldName && $this->association['type'] == ClassMetadata::ONE_TO_MANY) { + // Set back reference to owner + $this->typeClass->reflFields[$this->backRefFieldName] + ->setValue($element, $this->owner); + } + } + + /** + * Initializes the collection by loading its contents from the database + * if the collection is not yet initialized. + */ + public function initialize() + { + if ( ! $this->initialized && $this->association) { + if ($this->isDirty) { + // Has NEW objects added through add(). Remember them. + $newObjects = $this->coll->toArray(); + } + $this->coll->clear(); + $this->em->getUnitOfWork()->loadCollection($this); + $this->takeSnapshot(); + // Reattach NEW objects added through add(), if any. + if (isset($newObjects)) { + foreach ($newObjects as $obj) { + $this->coll->add($obj); + } + $this->isDirty = true; + } + $this->initialized = true; + } + } + + /** + * INTERNAL: + * Tells this collection to take a snapshot of its current state. + */ + public function takeSnapshot() + { + $this->snapshot = $this->coll->toArray(); + $this->isDirty = false; + } + + /** + * INTERNAL: + * Returns the last snapshot of the elements in the collection. + * + * @return array The last snapshot of the elements. + */ + public function getSnapshot() + { + return $this->snapshot; + } + + /** + * INTERNAL: + * getDeleteDiff + * + * @return array + */ + public function getDeleteDiff() + { + return array_udiff_assoc($this->snapshot, $this->coll->toArray(), + function($a, $b) {return $a === $b ? 0 : 1;}); + } + + /** + * INTERNAL: + * getInsertDiff + * + * @return array + */ + public function getInsertDiff() + { + return array_udiff_assoc($this->coll->toArray(), $this->snapshot, + function($a, $b) {return $a === $b ? 0 : 1;}); + } + + /** + * INTERNAL: Gets the association mapping of the collection. + * + * @return Doctrine\ORM\Mapping\AssociationMapping + */ + public function getMapping() + { + return $this->association; + } + + /** + * Marks this collection as changed/dirty. + */ + private function changed() + { + if ( ! $this->isDirty) { + $this->isDirty = true; + if ($this->association !== null && $this->association['isOwningSide'] && $this->association['type'] == ClassMetadata::MANY_TO_MANY && + $this->em->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) { + $this->em->getUnitOfWork()->scheduleForDirtyCheck($this->owner); + } + } + } + + /** + * Gets a boolean flag indicating whether this collection is dirty which means + * its state needs to be synchronized with the database. + * + * @return boolean TRUE if the collection is dirty, FALSE otherwise. + */ + public function isDirty() + { + return $this->isDirty; + } + + /** + * Sets a boolean flag, indicating whether this collection is dirty. + * + * @param boolean $dirty Whether the collection should be marked dirty or not. + */ + public function setDirty($dirty) + { + $this->isDirty = $dirty; + } + + /** + * Sets the initialized flag of the collection, forcing it into that state. + * + * @param boolean $bool + */ + public function setInitialized($bool) + { + $this->initialized = $bool; + } + + /** + * Checks whether this collection has been initialized. + * + * @return boolean + */ + public function isInitialized() + { + return $this->initialized; + } + + /** {@inheritdoc} */ + public function first() + { + $this->initialize(); + return $this->coll->first(); + } + + /** {@inheritdoc} */ + public function last() + { + $this->initialize(); + return $this->coll->last(); + } + + /** + * {@inheritdoc} + */ + public function remove($key) + { + // TODO: If the keys are persistent as well (not yet implemented) + // and the collection is not initialized and orphanRemoval is + // not used we can issue a straight SQL delete/update on the + // association (table). Without initializing the collection. + $this->initialize(); + $removed = $this->coll->remove($key); + if ($removed) { + $this->changed(); + if ($this->association !== null && $this->association['type'] == ClassMetadata::ONE_TO_MANY && + $this->association['orphanRemoval']) { + $this->em->getUnitOfWork()->scheduleOrphanRemoval($removed); + } + } + + return $removed; + } + + /** + * {@inheritdoc} + */ + public function removeElement($element) + { + // TODO: Assuming the identity of entities in a collection is always based + // on their primary key (there is no equals/hashCode in PHP), + // if the collection is not initialized, we could issue a straight + // SQL DELETE/UPDATE on the association (table) without initializing + // the collection. + /*if ( ! $this->initialized) { + $this->em->getUnitOfWork()->getCollectionPersister($this->association) + ->deleteRows($this, $element); + }*/ + + $this->initialize(); + $removed = $this->coll->removeElement($element); + if ($removed) { + $this->changed(); + if ($this->association !== null && $this->association['type'] == ClassMetadata::ONE_TO_MANY && + $this->association['orphanRemoval']) { + $this->em->getUnitOfWork()->scheduleOrphanRemoval($element); + } + } + return $removed; + } + + /** + * {@inheritdoc} + */ + public function containsKey($key) + { + $this->initialize(); + return $this->coll->containsKey($key); + } + + /** + * {@inheritdoc} + */ + public function contains($element) + { + /* DRAFT + if ($this->initialized) { + return $this->coll->contains($element); + } else { + if ($element is MANAGED) { + if ($this->coll->contains($element)) { + return true; + } + $exists = check db for existence; + if ($exists) { + $this->coll->add($element); + } + return $exists; + } + return false; + }*/ + + $this->initialize(); + return $this->coll->contains($element); + } + + /** + * {@inheritdoc} + */ + public function exists(Closure $p) + { + $this->initialize(); + return $this->coll->exists($p); + } + + /** + * {@inheritdoc} + */ + public function indexOf($element) + { + $this->initialize(); + return $this->coll->indexOf($element); + } + + /** + * {@inheritdoc} + */ + public function get($key) + { + $this->initialize(); + return $this->coll->get($key); + } + + /** + * {@inheritdoc} + */ + public function getKeys() + { + $this->initialize(); + return $this->coll->getKeys(); + } + + /** + * {@inheritdoc} + */ + public function getValues() + { + $this->initialize(); + return $this->coll->getValues(); + } + + /** + * {@inheritdoc} + */ + public function count() + { + $this->initialize(); + return $this->coll->count(); + } + + /** + * {@inheritdoc} + */ + public function set($key, $value) + { + $this->initialize(); + $this->coll->set($key, $value); + $this->changed(); + } + + /** + * {@inheritdoc} + */ + public function add($value) + { + $this->coll->add($value); + $this->changed(); + return true; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() + { + $this->initialize(); + return $this->coll->isEmpty(); + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $this->initialize(); + return $this->coll->getIterator(); + } + + /** + * {@inheritdoc} + */ + public function map(Closure $func) + { + $this->initialize(); + return $this->coll->map($func); + } + + /** + * {@inheritdoc} + */ + public function filter(Closure $p) + { + $this->initialize(); + return $this->coll->filter($p); + } + + /** + * {@inheritdoc} + */ + public function forAll(Closure $p) + { + $this->initialize(); + return $this->coll->forAll($p); + } + + /** + * {@inheritdoc} + */ + public function partition(Closure $p) + { + $this->initialize(); + return $this->coll->partition($p); + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + $this->initialize(); + return $this->coll->toArray(); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + if ($this->initialized && $this->isEmpty()) { + return; + } + if ($this->association['type'] == ClassMetadata::ONE_TO_MANY && $this->association['orphanRemoval']) { + foreach ($this->coll as $element) { + $this->em->getUnitOfWork()->scheduleOrphanRemoval($element); + } + } + $this->coll->clear(); + if ($this->association['isOwningSide']) { + $this->changed(); + $this->em->getUnitOfWork()->scheduleCollectionDeletion($this); + $this->takeSnapshot(); + } + } + + /** + * Called by PHP when this collection is serialized. Ensures that only the + * elements are properly serialized. + * + * @internal Tried to implement Serializable first but that did not work well + * with circular references. This solution seems simpler and works well. + */ + public function __sleep() + { + return array('coll', 'initialized'); + } + + /* ArrayAccess implementation */ + + /** + * @see containsKey() + */ + public function offsetExists($offset) + { + return $this->containsKey($offset); + } + + /** + * @see get() + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * @see add() + * @see set() + */ + public function offsetSet($offset, $value) + { + if ( ! isset($offset)) { + return $this->add($value); + } + return $this->set($offset, $value); + } + + /** + * @see remove() + */ + public function offsetUnset($offset) + { + return $this->remove($offset); + } + + public function key() + { + return $this->coll->key(); + } + + /** + * Gets the element of the collection at the current iterator position. + */ + public function current() + { + return $this->coll->current(); + } + + /** + * Moves the internal iterator position to the next element. + */ + public function next() + { + return $this->coll->next(); + } + + /** + * Retrieves the wrapped Collection instance. + */ + public function unwrap() + { + return $this->coll; + } + + /** + * Extract a slice of $length elements starting at position $offset from the Collection. + * + * If $length is null it returns all elements from $offset to the end of the Collection. + * Keys have to be preserved by this method. Calling this method will only return the + * selected slice and NOT change the elements contained in the collection slice is called on. + * + * @param int $offset + * @param int $length + * @return array + */ + public function slice($offset, $length = null) + { + $this->initialize(); + return $this->coll->slice($offset, $length); + } +} diff --git a/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/Doctrine/ORM/Persisters/AbstractCollectionPersister.php new file mode 100644 index 0000000..489bb82 --- /dev/null +++ b/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -0,0 +1,166 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\EntityManager, + Doctrine\ORM\PersistentCollection; + +/** + * Base class for all collection persisters. + * + * @since 2.0 + * @author Roman Borschel + */ +abstract class AbstractCollectionPersister +{ + /** + * @var EntityManager + */ + protected $_em; + + /** + * @var Doctrine\DBAL\Connection + */ + protected $_conn; + + /** + * @var Doctrine\ORM\UnitOfWork + */ + protected $_uow; + + /** + * Initializes a new instance of a class derived from AbstractCollectionPersister. + * + * @param Doctrine\ORM\EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + $this->_uow = $em->getUnitOfWork(); + $this->_conn = $em->getConnection(); + } + + /** + * Deletes the persistent state represented by the given collection. + * + * @param PersistentCollection $coll + */ + public function delete(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + if ( ! $mapping['isOwningSide']) { + return; // ignore inverse side + } + $sql = $this->_getDeleteSQL($coll); + $this->_conn->executeUpdate($sql, $this->_getDeleteSQLParameters($coll)); + } + + /** + * Gets the SQL statement for deleting the given collection. + * + * @param PersistentCollection $coll + */ + abstract protected function _getDeleteSQL(PersistentCollection $coll); + + /** + * Gets the SQL parameters for the corresponding SQL statement to delete + * the given collection. + * + * @param PersistentCollection $coll + */ + abstract protected function _getDeleteSQLParameters(PersistentCollection $coll); + + /** + * Updates the given collection, synchronizing it's state with the database + * by inserting, updating and deleting individual elements. + * + * @param PersistentCollection $coll + */ + public function update(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + if ( ! $mapping['isOwningSide']) { + return; // ignore inverse side + } + $this->deleteRows($coll); + //$this->updateRows($coll); + $this->insertRows($coll); + } + + public function deleteRows(PersistentCollection $coll) + { + $deleteDiff = $coll->getDeleteDiff(); + $sql = $this->_getDeleteRowSQL($coll); + foreach ($deleteDiff as $element) { + $this->_conn->executeUpdate($sql, $this->_getDeleteRowSQLParameters($coll, $element)); + } + } + + //public function updateRows(PersistentCollection $coll) + //{} + + public function insertRows(PersistentCollection $coll) + { + $insertDiff = $coll->getInsertDiff(); + $sql = $this->_getInsertRowSQL($coll); + foreach ($insertDiff as $element) { + $this->_conn->executeUpdate($sql, $this->_getInsertRowSQLParameters($coll, $element)); + } + } + + /** + * Gets the SQL statement used for deleting a row from the collection. + * + * @param PersistentCollection $coll + */ + abstract protected function _getDeleteRowSQL(PersistentCollection $coll); + + /** + * Gets the SQL parameters for the corresponding SQL statement to delete the given + * element from the given collection. + * + * @param PersistentCollection $coll + * @param mixed $element + */ + abstract protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element); + + /** + * Gets the SQL statement used for updating a row in the collection. + * + * @param PersistentCollection $coll + */ + abstract protected function _getUpdateRowSQL(PersistentCollection $coll); + + /** + * Gets the SQL statement used for inserting a row in the collection. + * + * @param PersistentCollection $coll + */ + abstract protected function _getInsertRowSQL(PersistentCollection $coll); + + /** + * Gets the SQL parameters for the corresponding SQL statement to insert the given + * element of the given collection into the database. + * + * @param PersistentCollection $coll + * @param mixed $element + */ + abstract protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element); +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php b/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php new file mode 100644 index 0000000..9be6de4 --- /dev/null +++ b/Doctrine/ORM/Persisters/AbstractEntityInheritancePersister.php @@ -0,0 +1,107 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\DBAL\Types\Type; + +/** + * Base class for entity persisters that implement a certain inheritance mapping strategy. + * All these persisters are assumed to use a discriminator column to discriminate entity + * types in the hierarchy. + * + * @author Roman Borschel + * @since 2.0 + */ +abstract class AbstractEntityInheritancePersister extends BasicEntityPersister +{ + /** + * Map from column names to class names that declare the field the column is mapped to. + * + * @var array + */ + private $_declaringClassMap = array(); + + /** + * {@inheritdoc} + */ + protected function _prepareInsertData($entity) + { + $data = parent::_prepareInsertData($entity); + // Populate the discriminator column + $discColumn = $this->_class->discriminatorColumn; + $this->_columnTypes[$discColumn['name']] = $discColumn['type']; + $data[$this->_getDiscriminatorColumnTableName()][$discColumn['name']] = $this->_class->discriminatorValue; + return $data; + } + + /** + * Gets the name of the table that contains the discriminator column. + * + * @return string The table name. + */ + abstract protected function _getDiscriminatorColumnTableName(); + + /** + * {@inheritdoc} + */ + protected function _processSQLResult(array $sqlResult) + { + $data = array(); + $discrColumnName = $this->_platform->getSQLResultCasing($this->_class->discriminatorColumn['name']); + $entityName = $this->_class->discriminatorMap[$sqlResult[$discrColumnName]]; + unset($sqlResult[$discrColumnName]); + foreach ($sqlResult as $column => $value) { + $realColumnName = $this->_resultColumnNames[$column]; + if (isset($this->_declaringClassMap[$column])) { + $class = $this->_declaringClassMap[$column]; + if ($class->name == $entityName || is_subclass_of($entityName, $class->name)) { + $field = $class->fieldNames[$realColumnName]; + if (isset($data[$field])) { + $data[$realColumnName] = $value; + } else { + $data[$field] = Type::getType($class->fieldMappings[$field]['type']) + ->convertToPHPValue($value, $this->_platform); + } + } + } else { + $data[$realColumnName] = $value; + } + } + + return array($entityName, $data); + } + + /** + * {@inheritdoc} + */ + protected function _getSelectColumnSQL($field, ClassMetadata $class) + { + $columnName = $class->columnNames[$field]; + $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); + if ( ! isset($this->_resultColumnNames[$columnAlias])) { + $this->_resultColumnNames[$columnAlias] = $columnName; + $this->_declaringClassMap[$columnAlias] = $class; + } + + return "$sql AS $columnAlias"; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/BasicEntityPersister.php b/Doctrine/ORM/Persisters/BasicEntityPersister.php new file mode 100644 index 0000000..2903ac9 --- /dev/null +++ b/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -0,0 +1,1201 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use PDO, + Doctrine\DBAL\LockMode, + Doctrine\DBAL\Types\Type, + Doctrine\ORM\ORMException, + Doctrine\ORM\OptimisticLockException, + Doctrine\ORM\EntityManager, + Doctrine\ORM\Query, + Doctrine\ORM\PersistentCollection, + Doctrine\ORM\Mapping\MappingException, + Doctrine\ORM\Mapping\ClassMetadata; + +/** + * A BasicEntityPersiter maps an entity to a single table in a relational database. + * + * A persister is always responsible for a single entity type. + * + * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent + * state of entities onto a relational database when the UnitOfWork is committed, + * as well as for basic querying of entities and their associations (not DQL). + * + * The persisting operations that are invoked during a commit of a UnitOfWork to + * persist the persistent entity state are: + * + * - {@link addInsert} : To schedule an entity for insertion. + * - {@link executeInserts} : To execute all scheduled insertions. + * - {@link update} : To update the persistent state of an entity. + * - {@link delete} : To delete the persistent state of an entity. + * + * As can be seen from the above list, insertions are batched and executed all at once + * for increased efficiency. + * + * The querying operations invoked during a UnitOfWork, either through direct find + * requests or lazy-loading, are the following: + * + * - {@link load} : Loads (the state of) a single, managed entity. + * - {@link loadAll} : Loads multiple, managed entities. + * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading). + * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading). + * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading). + * + * The BasicEntityPersister implementation provides the default behavior for + * persisting and querying entities that are mapped to a single database table. + * + * Subclasses can be created to provide custom persisting and querying strategies, + * i.e. spanning multiple tables. + * + * @author Roman Borschel + * @author Giorgio Sironi + * @since 2.0 + */ +class BasicEntityPersister +{ + /** + * Metadata object that describes the mapping of the mapped entity class. + * + * @var Doctrine\ORM\Mapping\ClassMetadata + */ + protected $_class; + + /** + * The underlying DBAL Connection of the used EntityManager. + * + * @var Doctrine\DBAL\Connection $conn + */ + protected $_conn; + + /** + * The database platform. + * + * @var Doctrine\DBAL\Platforms\AbstractPlatform + */ + protected $_platform; + + /** + * The EntityManager instance. + * + * @var Doctrine\ORM\EntityManager + */ + protected $_em; + + /** + * Queued inserts. + * + * @var array + */ + protected $_queuedInserts = array(); + + /** + * Case-sensitive mappings of column names as they appear in an SQL result set + * to column names as they are defined in the mapping. This is necessary because different + * RDBMS vendors return column names in result sets in different casings. + * + * @var array + */ + protected $_resultColumnNames = array(); + + /** + * The map of column names to DBAL mapping types of all prepared columns used + * when INSERTing or UPDATEing an entity. + * + * @var array + * @see _prepareInsertData($entity) + * @see _prepareUpdateData($entity) + */ + protected $_columnTypes = array(); + + /** + * The INSERT SQL statement used for entities handled by this persister. + * This SQL is only generated once per request, if at all. + * + * @var string + */ + private $_insertSql; + + /** + * The SELECT column list SQL fragment used for querying entities by this persister. + * This SQL fragment is only generated once per request, if at all. + * + * @var string + */ + protected $_selectColumnListSql; + + /** + * Counter for creating unique SQL table and column aliases. + * + * @var integer + */ + protected $_sqlAliasCounter = 0; + + /** + * Map from class names (FQCN) to the corresponding generated SQL table aliases. + * + * @var array + */ + protected $_sqlTableAliases = array(); + + /** + * Initializes a new BasicEntityPersister that uses the given EntityManager + * and persists instances of the class described by the given ClassMetadata descriptor. + * + * @param Doctrine\ORM\EntityManager $em + * @param Doctrine\ORM\Mapping\ClassMetadata $class + */ + public function __construct(EntityManager $em, ClassMetadata $class) + { + $this->_em = $em; + $this->_class = $class; + $this->_conn = $em->getConnection(); + $this->_platform = $this->_conn->getDatabasePlatform(); + } + + /** + * Adds an entity to the queued insertions. + * The entity remains queued until {@link executeInserts} is invoked. + * + * @param object $entity The entity to queue for insertion. + */ + public function addInsert($entity) + { + $this->_queuedInserts[spl_object_hash($entity)] = $entity; + } + + /** + * Executes all queued entity insertions and returns any generated post-insert + * identifiers that were created as a result of the insertions. + * + * If no inserts are queued, invoking this method is a NOOP. + * + * @return array An array of any generated post-insert IDs. This will be an empty array + * if the entity class does not use the IDENTITY generation strategy. + */ + public function executeInserts() + { + if ( ! $this->_queuedInserts) { + return; + } + + $postInsertIds = array(); + $idGen = $this->_class->idGenerator; + $isPostInsertId = $idGen->isPostInsertGenerator(); + + $stmt = $this->_conn->prepare($this->_getInsertSQL()); + $tableName = $this->_class->table['name']; + + foreach ($this->_queuedInserts as $entity) { + $insertData = $this->_prepareInsertData($entity); + + if (isset($insertData[$tableName])) { + $paramIndex = 1; + foreach ($insertData[$tableName] as $column => $value) { + $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$column]); + } + } + + $stmt->execute(); + + if ($isPostInsertId) { + $id = $idGen->generate($this->_em, $entity); + $postInsertIds[$id] = $entity; + } else { + $id = $this->_class->getIdentifierValues($entity); + } + + if ($this->_class->isVersioned) { + $this->_assignDefaultVersionValue($this->_class, $entity, $id); + } + } + + $stmt->closeCursor(); + $this->_queuedInserts = array(); + + return $postInsertIds; + } + + /** + * Retrieves the default version value which was created + * by the preceding INSERT statement and assigns it back in to the + * entities version field. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + * @param object $entity + * @param mixed $id + */ + protected function _assignDefaultVersionValue($class, $entity, $id) + { + $versionField = $this->_class->versionField; + $identifier = $this->_class->getIdentifierColumnNames(); + $versionFieldColumnName = $this->_class->getColumnName($versionField); + //FIXME: Order with composite keys might not be correct + $sql = "SELECT " . $versionFieldColumnName . " FROM " . $class->getQuotedTableName($this->_platform) + . " WHERE " . implode(' = ? AND ', $identifier) . " = ?"; + $value = $this->_conn->fetchColumn($sql, array_values((array)$id)); + + $value = Type::getType($class->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->_platform); + $this->_class->setFieldValue($entity, $versionField, $value); + } + + /** + * Updates a managed entity. The entity is updated according to its current changeset + * in the running UnitOfWork. If there is no changeset, nothing is updated. + * + * The data to update is retrieved through {@link _prepareUpdateData}. + * Subclasses that override this method are supposed to obtain the update data + * in the same way, through {@link _prepareUpdateData}. + * + * Subclasses are also supposed to take care of versioning when overriding this method, + * if necessary. The {@link _updateTable} method can be used to apply the data retrieved + * from {@_prepareUpdateData} on the target tables, thereby optionally applying versioning. + * + * @param object $entity The entity to update. + */ + public function update($entity) + { + $updateData = $this->_prepareUpdateData($entity); + $tableName = $this->_class->table['name']; + if (isset($updateData[$tableName]) && $updateData[$tableName]) { + $this->_updateTable( + $entity, $this->_class->getQuotedTableName($this->_platform), + $updateData[$tableName], $this->_class->isVersioned + ); + + if ($this->_class->isVersioned) { + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + $this->_assignDefaultVersionValue($this->_class, $entity, $id); + } + } + } + + /** + * Performs an UPDATE statement for an entity on a specific table. + * The UPDATE can optionally be versioned, which requires the entity to have a version field. + * + * @param object $entity The entity object being updated. + * @param string $quotedTableName The quoted name of the table to apply the UPDATE on. + * @param array $updateData The map of columns to update (column => value). + * @param boolean $versioned Whether the UPDATE should be versioned. + */ + protected final function _updateTable($entity, $quotedTableName, array $updateData, $versioned = false) + { + $set = $params = $types = array(); + + foreach ($updateData as $columnName => $value) { + if (isset($this->_class->fieldNames[$columnName])) { + $set[] = $this->_class->getQuotedColumnName($this->_class->fieldNames[$columnName], $this->_platform) . ' = ?'; + } else { + $set[] = $columnName . ' = ?'; + } + $params[] = $value; + $types[] = $this->_columnTypes[$columnName]; + } + + $where = array(); + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + foreach ($this->_class->identifier as $idField) { + $where[] = $this->_class->getQuotedColumnName($idField, $this->_platform); + $params[] = $id[$idField]; + $types[] = $this->_class->fieldMappings[$idField]['type']; + } + + if ($versioned) { + $versionField = $this->_class->versionField; + $versionFieldType = $this->_class->fieldMappings[$versionField]['type']; + $versionColumn = $this->_class->getQuotedColumnName($versionField, $this->_platform); + if ($versionFieldType == Type::INTEGER) { + $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; + } else if ($versionFieldType == Type::DATETIME) { + $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; + } + $where[] = $versionColumn; + $params[] = $this->_class->reflFields[$versionField]->getValue($entity); + $types[] = $this->_class->fieldMappings[$versionField]['type']; + } + + $sql = "UPDATE $quotedTableName SET " . implode(', ', $set) + . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; + + $result = $this->_conn->executeUpdate($sql, $params, $types); + + if ($this->_class->isVersioned && ! $result) { + throw OptimisticLockException::lockFailed($entity); + } + } + + /** + * @todo Add check for platform if it supports foreign keys/cascading. + * @param array $identifier + * @return void + */ + protected function deleteJoinTableRecords($identifier) + { + foreach ($this->_class->associationMappings as $mapping) { + if ($mapping['type'] == ClassMetadata::MANY_TO_MANY) { + // @Todo this only covers scenarios with no inheritance or of the same level. Is there something + // like self-referential relationship between different levels of an inheritance hierachy? I hope not! + $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']); + + if ( ! $mapping['isOwningSide']) { + $relatedClass = $this->_em->getClassMetadata($mapping['targetEntity']); + $mapping = $relatedClass->associationMappings[$mapping['mappedBy']]; + $keys = array_keys($mapping['relationToTargetKeyColumns']); + if ($selfReferential) { + $otherKeys = array_keys($mapping['relationToSourceKeyColumns']); + } + } else { + $keys = array_keys($mapping['relationToSourceKeyColumns']); + if ($selfReferential) { + $otherKeys = array_keys($mapping['relationToTargetKeyColumns']); + } + } + + if ( ! isset($mapping['isOnDeleteCascade'])) { + $this->_conn->delete($mapping['joinTable']['name'], array_combine($keys, $identifier)); + + if ($selfReferential) { + $this->_conn->delete($mapping['joinTable']['name'], array_combine($otherKeys, $identifier)); + } + } + } + } + } + + /** + * Deletes a managed entity. + * + * The entity to delete must be managed and have a persistent identifier. + * The deletion happens instantaneously. + * + * Subclasses may override this method to customize the semantics of entity deletion. + * + * @param object $entity The entity to delete. + */ + public function delete($entity) + { + $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + $this->deleteJoinTableRecords($identifier); + + $id = array_combine($this->_class->getIdentifierColumnNames(), $identifier); + $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id); + } + + /** + * Prepares the changeset of an entity for database insertion (UPDATE). + * + * The changeset is obtained from the currently running UnitOfWork. + * + * During this preparation the array that is passed as the second parameter is filled with + * => pairs, grouped by table name. + * + * Example: + * + * array( + * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), + * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), + * ... + * ) + * + * + * @param object $entity The entity for which to prepare the data. + * @return array The prepared data. + */ + protected function _prepareUpdateData($entity) + { + $result = array(); + $uow = $this->_em->getUnitOfWork(); + + if ($versioned = $this->_class->isVersioned) { + $versionField = $this->_class->versionField; + } + + foreach ($uow->getEntityChangeSet($entity) as $field => $change) { + if ($versioned && $versionField == $field) { + continue; + } + + $oldVal = $change[0]; + $newVal = $change[1]; + + if (isset($this->_class->associationMappings[$field])) { + $assoc = $this->_class->associationMappings[$field]; + // Only owning side of x-1 associations can have a FK column. + if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) { + continue; + } + + if ($newVal !== null) { + $oid = spl_object_hash($newVal); + if (isset($this->_queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) { + // The associated entity $newVal is not yet persisted, so we must + // set $newVal = null, in order to insert a null value and schedule an + // extra update on the UnitOfWork. + $uow->scheduleExtraUpdate($entity, array( + $field => array(null, $newVal) + )); + $newVal = null; + } + } + + if ($newVal !== null) { + $newValId = $uow->getEntityIdentifier($newVal); + } + + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + $owningTable = $this->getOwningTable($field); + + foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + if ($newVal === null) { + $result[$owningTable][$sourceColumn] = null; + } else { + $result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]]; + } + $this->_columnTypes[$sourceColumn] = $targetClass->getTypeOfColumn($targetColumn); + } + } else { + $columnName = $this->_class->columnNames[$field]; + $this->_columnTypes[$columnName] = $this->_class->fieldMappings[$field]['type']; + $result[$this->getOwningTable($field)][$columnName] = $newVal; + } + } + return $result; + } + + /** + * Prepares the data changeset of a managed entity for database insertion (initial INSERT). + * The changeset of the entity is obtained from the currently running UnitOfWork. + * + * The default insert data preparation is the same as for updates. + * + * @param object $entity The entity for which to prepare the data. + * @return array The prepared data for the tables to update. + * @see _prepareUpdateData + */ + protected function _prepareInsertData($entity) + { + return $this->_prepareUpdateData($entity); + } + + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * The default implementation in BasicEntityPersister always returns the name + * of the table the entity type of this persister is mapped to, since an entity + * is always persisted to a single table with a BasicEntityPersister. + * + * @param string $fieldName The field name. + * @return string The table name. + */ + public function getOwningTable($fieldName) + { + return $this->_class->table['name']; + } + + /** + * Loads an entity by a list of field criteria. + * + * @param array $criteria The criteria by which to load the entity. + * @param object $entity The entity to load the data into. If not specified, + * a new entity is created. + * @param $assoc The association that connects the entity to load to another entity, if any. + * @param array $hints Hints for entity creation. + * @param int $lockMode + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + */ + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0) + { + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc, $lockMode); + $stmt = $this->_conn->executeQuery($sql, array_values($criteria)); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + return $this->_createEntity($result, $entity, $hints); + } + + /** + * Loads an entity of this persister's mapped class as part of a single-valued + * association from another entity. + * + * @param array $assoc The association to load. + * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). + * @param object $targetEntity The existing ghost entity (proxy) to load, if any. + * @param array $identifier The identifier of the entity to load. Must be provided if + * the association to load represents the owning side, otherwise + * the identifier is derived from the $sourceEntity. + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + */ + public function loadOneToOneEntity(array $assoc, $sourceEntity, $targetEntity, array $identifier = array()) + { + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + + if ($assoc['isOwningSide']) { + $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']); + + // Mark inverse side as fetched in the hints, otherwise the UoW would + // try to load it in a separate query (remember: to-one inverse sides can not be lazy). + $hints = array(); + if ($isInverseSingleValued) { + $hints['fetched'][$targetClass->name][$assoc['inversedBy']] = true; + if ($targetClass->subClasses) { + foreach ($targetClass->subClasses as $targetSubclassName) { + $hints['fetched'][$targetSubclassName][$assoc['inversedBy']] = true; + } + } + } + /* cascade read-only status + if ($this->_em->getUnitOfWork()->isReadOnly($sourceEntity)) { + $hints[Query::HINT_READ_ONLY] = true; + } + */ + + $targetEntity = $this->load($identifier, $targetEntity, $assoc, $hints); + + // Complete bidirectional association, if necessary + if ($targetEntity !== null && $isInverseSingleValued) { + $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity); + } + } else { + $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); + $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']); + // TRICKY: since the association is specular source and target are flipped + foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { + if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { + $identifier[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } else { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, $sourceKeyColumn + ); + } + } + + $targetEntity = $this->load($identifier, $targetEntity, $assoc); + + if ($targetEntity !== null) { + $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity); + } + } + + return $targetEntity; + } + + /** + * Refreshes a managed entity. + * + * @param array $id The identifier of the entity as an associative array from + * column or field names to values. + * @param object $entity The entity to refresh. + */ + public function refresh(array $id, $entity) + { + $sql = $this->_getSelectEntitiesSQL($id); + $stmt = $this->_conn->executeQuery($sql, array_values($id)); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + $metaColumns = array(); + $newData = array(); + + // Refresh simple state + foreach ($result as $column => $value) { + $column = $this->_resultColumnNames[$column]; + if (isset($this->_class->fieldNames[$column])) { + $fieldName = $this->_class->fieldNames[$column]; + $newValue = $this->_conn->convertToPHPValue($value, $this->_class->fieldMappings[$fieldName]['type']); + $this->_class->reflFields[$fieldName]->setValue($entity, $newValue); + $newData[$fieldName] = $newValue; + } else { + $metaColumns[$column] = $value; + } + } + + // Refresh associations + foreach ($this->_class->associationMappings as $field => $assoc) { + $value = $this->_class->reflFields[$field]->getValue($entity); + if ($assoc['type'] & ClassMetadata::TO_ONE) { + if ($value instanceof Proxy && ! $value->__isInitialized__) { + continue; // skip uninitialized proxies + } + + if ($assoc['isOwningSide']) { + $joinColumnValues = array(); + foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { + if ($metaColumns[$srcColumn] !== null) { + $joinColumnValues[$targetColumn] = $metaColumns[$srcColumn]; + } + } + if ( ! $joinColumnValues && $value !== null) { + $this->_class->reflFields[$field]->setValue($entity, null); + $newData[$field] = null; + } else if ($value !== null) { + // Check identity map first, if the entity is not there, + // place a proxy in there instead. + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + if ($found = $this->_em->getUnitOfWork()->tryGetById($joinColumnValues, $targetClass->rootEntityName)) { + $this->_class->reflFields[$field]->setValue($entity, $found); + // Complete inverse side, if necessary. + if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) { + $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']]; + $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($found, $entity); + } + $newData[$field] = $found; + } else { + // FIXME: What is happening with subClassees here? + $proxy = $this->_em->getProxyFactory()->getProxy($assoc['targetEntity'], $joinColumnValues); + $this->_class->reflFields[$field]->setValue($entity, $proxy); + $newData[$field] = $proxy; + $this->_em->getUnitOfWork()->registerManaged($proxy, $joinColumnValues, array()); + } + } + } else { + // Inverse side of 1-1/1-x can never be lazy. + //$newData[$field] = $assoc->load($entity, null, $this->_em); + $newData[$field] = $this->_em->getUnitOfWork()->getEntityPersister($assoc['targetEntity']) + ->loadOneToOneEntity($assoc, $entity, null); + } + } else if ($value instanceof PersistentCollection && $value->isInitialized()) { + $value->setInitialized(false); + // no matter if dirty or non-dirty entities are already loaded, smoke them out! + // the beauty of it being, they are still in the identity map + $value->unwrap()->clear(); + $newData[$field] = $value; + } + } + + $this->_em->getUnitOfWork()->setOriginalEntityData($entity, $newData); + } + + /** + * Loads a list of entities by a list of field criteria. + * + * @param array $criteria + * @return array + */ + public function loadAll(array $criteria = array()) + { + $entities = array(); + $sql = $this->_getSelectEntitiesSQL($criteria); + $stmt = $this->_conn->executeQuery($sql, array_values($criteria)); + $result = $stmt->fetchAll(PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + foreach ($result as $row) { + $entities[] = $this->_createEntity($row); + } + + return $entities; + } + + /** + * Loads a collection of entities of a many-to-many association. + * + * @param ManyToManyMapping $assoc The association mapping of the association being loaded. + * @param object $sourceEntity The entity that owns the collection. + * @param PersistentCollection $coll The collection to fill. + */ + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $criteria = array(); + $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); + $joinTableConditions = array(); + if ($assoc['isOwningSide']) { + foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { + if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { + $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } else { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, $sourceKeyColumn + ); + } + } + } else { + $owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']]; + // TRICKY: since the association is inverted source and target are flipped + foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) { + if (isset($sourceClass->fieldNames[$sourceKeyColumn])) { + $criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } else { + throw MappingException::joinColumnMustPointToMappedField( + $sourceClass->name, $sourceKeyColumn + ); + } + } + } + + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); + $stmt = $this->_conn->executeQuery($sql, array_values($criteria)); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + $stmt->closeCursor(); + } + + /** + * Creates or fills a single entity object from an SQL result. + * + * @param $result The SQL result. + * @param object $entity The entity object to fill, if any. + * @param array $hints Hints for entity creation. + * @return object The filled and managed entity object or NULL, if the SQL result is empty. + */ + private function _createEntity($result, $entity = null, array $hints = array()) + { + if ($result === false) { + return null; + } + + list($entityName, $data) = $this->_processSQLResult($result); + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + $id = array(); + if ($this->_class->isIdentifierComposite) { + foreach ($this->_class->identifier as $fieldName) { + $id[$fieldName] = $data[$fieldName]; + } + } else { + $id = array($this->_class->identifier[0] => $data[$this->_class->identifier[0]]); + } + $this->_em->getUnitOfWork()->registerManaged($entity, $id, $data); + } + + return $this->_em->getUnitOfWork()->createEntity($entityName, $data, $hints); + } + + /** + * Processes an SQL result set row that contains data for an entity of the type + * this persister is responsible for. + * + * Subclasses are supposed to override this method if they need to change the + * hydration procedure for entities loaded through basic find operations or + * lazy-loading (not DQL). + * + * @param array $sqlResult The SQL result set row to process. + * @return array A tuple where the first value is the actual type of the entity and + * the second value the prepared data of the entity (a map from field + * names to values). + */ + protected function _processSQLResult(array $sqlResult) + { + $data = array(); + foreach ($sqlResult as $column => $value) { + $column = $this->_resultColumnNames[$column]; + if (isset($this->_class->fieldNames[$column])) { + $field = $this->_class->fieldNames[$column]; + if (isset($data[$field])) { + $data[$column] = $value; + } else { + $data[$field] = Type::getType($this->_class->fieldMappings[$field]['type']) + ->convertToPHPValue($value, $this->_platform); + } + } else { + $data[$column] = $value; + } + } + + return array($this->_class->name, $data); + } + + /** + * Gets the SELECT SQL to select one or more entities by a set of field criteria. + * + * @param array $criteria + * @param AssociationMapping $assoc + * @param string $orderBy + * @param int $lockMode + * @return string + * @todo Refactor: _getSelectSQL(...) + */ + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0) + { + $joinSql = $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ? + $this->_getSelectManyToManyJoinSQL($assoc) : ''; + + $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); + + $orderBySql = $assoc !== null && isset($assoc['orderBy']) ? + $this->_getCollectionOrderBySQL($assoc['orderBy'], $this->_getSQLTableAlias($this->_class->name)) + : ''; + + $lockSql = ''; + if ($lockMode == LockMode::PESSIMISTIC_READ) { + $lockSql = ' ' . $this->_platform->getReadLockSql(); + } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { + $lockSql = ' ' . $this->_platform->getWriteLockSql(); + } + + return 'SELECT ' . $this->_getSelectColumnListSQL() + . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' + . $this->_getSQLTableAlias($this->_class->name), $lockMode) + . $joinSql + . ($conditionSql ? ' WHERE ' . $conditionSql : '') + . $orderBySql + . $lockSql; + } + + /** + * Gets the ORDER BY SQL snippet for ordered collections. + * + * @param array $orderBy + * @param string $baseTableAlias + * @return string + * @todo Rename: _getOrderBySQL + */ + protected final function _getCollectionOrderBySQL(array $orderBy, $baseTableAlias) + { + $orderBySql = ''; + foreach ($orderBy as $fieldName => $orientation) { + if ( ! isset($this->_class->fieldMappings[$fieldName])) { + ORMException::unrecognizedField($fieldName); + } + + $tableAlias = isset($this->_class->fieldMappings[$fieldName]['inherited']) ? + $this->_getSQLTableAlias($this->_class->fieldMappings[$fieldName]['inherited']) + : $baseTableAlias; + + $columnName = $this->_class->getQuotedColumnName($fieldName, $this->_platform); + $orderBySql .= $orderBySql ? ', ' : ' ORDER BY '; + $orderBySql .= $tableAlias . '.' . $columnName . ' ' . $orientation; + } + + return $orderBySql; + } + + /** + * Gets the SQL fragment with the list of columns to select when querying for + * an entity in this persister. + * + * Subclasses should override this method to alter or change the select column + * list SQL fragment. Note that in the implementation of BasicEntityPersister + * the resulting SQL fragment is generated only once and cached in {@link _selectColumnListSql}. + * Subclasses may or may not do the same. + * + * @return string The SQL fragment. + * @todo Rename: _getSelectColumnsSQL() + */ + protected function _getSelectColumnListSQL() + { + if ($this->_selectColumnListSql !== null) { + return $this->_selectColumnListSql; + } + + $columnList = ''; + + // Add regular columns to select list + foreach ($this->_class->fieldNames as $field) { + if ($columnList) $columnList .= ', '; + $columnList .= $this->_getSelectColumnSQL($field, $this->_class); + } + + $this->_selectColumnListSql = $columnList . $this->_getSelectJoinColumnsSQL($this->_class); + + return $this->_selectColumnListSql; + } + + /** + * Gets the SQL join fragment used when selecting entities from a + * many-to-many association. + * + * @param ManyToManyMapping $manyToMany + * @return string + */ + protected function _getSelectManyToManyJoinSQL(array $manyToMany) + { + if ($manyToMany['isOwningSide']) { + $owningAssoc = $manyToMany; + $joinClauses = $manyToMany['relationToTargetKeyColumns']; + } else { + $owningAssoc = $this->_em->getClassMetadata($manyToMany['targetEntity'])->associationMappings[$manyToMany['mappedBy']]; + $joinClauses = $owningAssoc['relationToSourceKeyColumns']; + } + + $joinTableName = $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform); + + $joinSql = ''; + foreach ($joinClauses as $joinTableColumn => $sourceColumn) { + if ($joinSql != '') $joinSql .= ' AND '; + $joinSql .= $this->_getSQLTableAlias($this->_class->name) . + '.' . $this->_class->getQuotedColumnName($this->_class->fieldNames[$sourceColumn], $this->_platform) . ' = ' + . $joinTableName . '.' . $joinTableColumn; + } + + return " INNER JOIN $joinTableName ON $joinSql"; + } + + /** + * Gets the INSERT SQL used by the persister to persist a new entity. + * + * @return string + */ + protected function _getInsertSQL() + { + if ($this->_insertSql === null) { + $insertSql = ''; + $columns = $this->_getInsertColumnList(); + if (empty($columns)) { + $insertSql = $this->_platform->getEmptyIdentityInsertSQL( + $this->_class->getQuotedTableName($this->_platform), + $this->_class->getQuotedColumnName($this->_class->identifier[0], $this->_platform) + ); + } else { + $columns = array_unique($columns); + $values = array_fill(0, count($columns), '?'); + + $insertSql = 'INSERT INTO ' . $this->_class->getQuotedTableName($this->_platform) + . ' (' . implode(', ', $columns) . ') ' + . 'VALUES (' . implode(', ', $values) . ')'; + } + $this->_insertSql = $insertSql; + } + return $this->_insertSql; + } + + /** + * Gets the list of columns to put in the INSERT SQL statement. + * + * Subclasses should override this method to alter or change the list of + * columns placed in the INSERT statements used by the persister. + * + * @return array The list of columns. + */ + protected function _getInsertColumnList() + { + $columns = array(); + foreach ($this->_class->reflFields as $name => $field) { + if ($this->_class->isVersioned && $this->_class->versionField == $name) { + continue; + } + if (isset($this->_class->associationMappings[$name])) { + $assoc = $this->_class->associationMappings[$name]; + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { + $columns[] = $sourceCol; + } + } + } else if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || + $this->_class->identifier[0] != $name) { + $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform); + } + } + + return $columns; + } + + /** + * Gets the SQL snippet of a qualified column name for the given field name. + * + * @param string $field The field name. + * @param ClassMetadata $class The class that declares this field. The table this class is + * mapped to must own the column for the given field. + */ + protected function _getSelectColumnSQL($field, ClassMetadata $class) + { + $columnName = $class->columnNames[$field]; + $sql = $this->_getSQLTableAlias($class->name) . '.' . $class->getQuotedColumnName($field, $this->_platform); + $columnAlias = $this->_platform->getSQLResultCasing($columnName . $this->_sqlAliasCounter++); + if ( ! isset($this->_resultColumnNames[$columnAlias])) { + $this->_resultColumnNames[$columnAlias] = $columnName; + } + + return "$sql AS $columnAlias"; + } + + /** + * Gets the SQL snippet for all join columns of the given class that are to be + * placed in an SQL SELECT statement. + * + * @param $class + * @return string + * @todo Not reused... inline? + */ + private function _getSelectJoinColumnsSQL(ClassMetadata $class) + { + $sql = ''; + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $sql .= ', ' . $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + if ( ! isset($this->_resultColumnNames[$resultColumnName])) { + $this->_resultColumnNames[$resultColumnName] = $srcColumn; + } + } + } + } + + return $sql; + } + + /** + * Gets the SQL table alias for the given class name. + * + * @param string $className + * @return string The SQL table alias. + * @todo Reconsider. Binding table aliases to class names is not such a good idea. + */ + protected function _getSQLTableAlias($className) + { + if (isset($this->_sqlTableAliases[$className])) { + return $this->_sqlTableAliases[$className]; + } + $tableAlias = 't' . $this->_sqlAliasCounter++; + $this->_sqlTableAliases[$className] = $tableAlias; + + return $tableAlias; + } + + /** + * Lock all rows of this entity matching the given criteria with the specified pessimistic lock mode + * + * @param array $criteria + * @param int $lockMode + * @return void + */ + public function lock(array $criteria, $lockMode) + { + $conditionSql = $this->_getSelectConditionSQL($criteria); + + if ($lockMode == LockMode::PESSIMISTIC_READ) { + $lockSql = $this->_platform->getReadLockSql(); + } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { + $lockSql = $this->_platform->getWriteLockSql(); + } + + $sql = 'SELECT 1 ' + . $this->_platform->appendLockHint($this->getLockTablesSql(), $lockMode) + . ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ' . $lockSql; + $params = array_values($criteria); + $this->_conn->executeQuery($sql, $params); + } + + /** + * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister. + * + * @return string + */ + protected function getLockTablesSql() + { + return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' + . $this->_getSQLTableAlias($this->_class->name); + } + + /** + * Gets the conditional SQL fragment used in the WHERE clause when selecting + * entities in this persister. + * + * Subclasses are supposed to override this method if they intend to change + * or alter the criteria by which entities are selected. + * + * @param array $criteria + * @param AssociationMapping $assoc + * @return string + */ + protected function _getSelectConditionSQL(array $criteria, $assoc = null) + { + $conditionSql = ''; + foreach ($criteria as $field => $value) { + $conditionSql .= $conditionSql ? ' AND ' : ''; + + if (isset($this->_class->columnNames[$field])) { + if (isset($this->_class->fieldMappings[$field]['inherited'])) { + $conditionSql .= $this->_getSQLTableAlias($this->_class->fieldMappings[$field]['inherited']) . '.'; + } else { + $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.'; + } + $conditionSql .= $this->_class->getQuotedColumnName($field, $this->_platform); + } else if (isset($this->_class->associationMappings[$field])) { + if (!$this->_class->associationMappings[$field]['isOwningSide']) { + throw ORMException::invalidFindByInverseAssociation($this->_class->name, $field); + } + + if (isset($this->_class->associationMappings[$field]['inherited'])) { + $conditionSql .= $this->_getSQLTableAlias($this->_class->associationMappings[$field]['inherited']) . '.'; + } else { + $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.'; + } + + + $conditionSql .= $this->_class->associationMappings[$field]['joinColumns'][0]['name']; + } else if ($assoc !== null) { + if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { + $owningAssoc = $assoc['isOwningSide'] ? $assoc : $this->_em->getClassMetadata($assoc['targetEntity']) + ->associationMappings[$assoc['mappedBy']]; + $conditionSql .= $this->_class->getQuotedJoinTableName($owningAssoc, $this->_platform) . '.' . $field; + } else { + $conditionSql .= $field; + } + } else { + throw ORMException::unrecognizedField($field); + } + $conditionSql .= ' = ?'; + } + return $conditionSql; + } + + /** + * Loads a collection of entities in a one-to-many association. + * + * @param OneToManyMapping $assoc + * @param array $criteria The criteria by which to select the entities. + * @param PersistentCollection The collection to load/fill. + */ + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $criteria = array(); + $owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']]; + $sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']); + foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) { + $criteria[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); + } + + $sql = $this->_getSelectEntitiesSQL($criteria, $assoc); + $params = array_values($criteria); + $stmt = $this->_conn->executeQuery($sql, $params); + while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) { + $coll->hydrateAdd($this->_createEntity($result)); + } + $stmt->closeCursor(); + } + + /** + * Checks whether the given managed entity exists in the database. + * + * @param object $entity + * @return boolean TRUE if the entity exists in the database, FALSE otherwise. + */ + public function exists($entity) + { + $criteria = $this->_class->getIdentifierValues($entity); + $sql = 'SELECT 1 FROM ' . $this->_class->getQuotedTableName($this->_platform) + . ' ' . $this->_getSQLTableAlias($this->_class->name) + . ' WHERE ' . $this->_getSelectConditionSQL($criteria); + + return (bool) $this->_conn->fetchColumn($sql, array_values($criteria)); + } + + //TODO + /*protected function _getOneToOneEagerFetchSQL() + { + + }*/ +} diff --git a/Doctrine/ORM/Persisters/ElementCollectionPersister.php b/Doctrine/ORM/Persisters/ElementCollectionPersister.php new file mode 100644 index 0000000..688edeb --- /dev/null +++ b/Doctrine/ORM/Persisters/ElementCollectionPersister.php @@ -0,0 +1,33 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +/** + * Persister for collections of basic elements / value types. + * + * @author robo + * @todo Implementation once support for collections of basic elements (i.e. strings) is added. + */ +abstract class ElementCollectionPersister extends AbstractCollectionPersister +{ + //put your code here +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/Doctrine/ORM/Persisters/JoinedSubclassPersister.php new file mode 100644 index 0000000..4cbe11e --- /dev/null +++ b/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -0,0 +1,425 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\ORMException, + Doctrine\ORM\Mapping\ClassMetadata; + +/** + * The joined subclass persister maps a single entity instance to several tables in the + * database as it is defined by the Class Table Inheritance strategy. + * + * @author Roman Borschel + * @since 2.0 + * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html + */ +class JoinedSubclassPersister extends AbstractEntityInheritancePersister +{ + /** + * Map that maps column names to the table names that own them. + * This is mainly a temporary cache, used during a single request. + * + * @var array + */ + private $_owningTableMap = array(); + + /** + * Map of table to quoted table names. + * + * @var array + */ + private $_quotedTableMap = array(); + + /** + * {@inheritdoc} + */ + protected function _getDiscriminatorColumnTableName() + { + if ($this->_class->name == $this->_class->rootEntityName) { + return $this->_class->table['name']; + } else { + return $this->_em->getClassMetadata($this->_class->rootEntityName)->table['name']; + } + } + + /** + * This function finds the ClassMetadata instance in an inheritance hierarchy + * that is responsible for enabling versioning. + * + * @return Doctrine\ORM\Mapping\ClassMetadata + */ + private function _getVersionedClassMetadata() + { + if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) { + $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited']; + return $this->_em->getClassMetadata($definingClassName); + } + return $this->_class; + } + + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * @param string $fieldName + * @return string + * @override + */ + public function getOwningTable($fieldName) + { + if (!isset($this->_owningTableMap[$fieldName])) { + if (isset($this->_class->associationMappings[$fieldName]['inherited'])) { + $cm = $this->_em->getClassMetadata($this->_class->associationMappings[$fieldName]['inherited']); + } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) { + $cm = $this->_em->getClassMetadata($this->_class->fieldMappings[$fieldName]['inherited']); + } else { + $cm = $this->_class; + } + $this->_owningTableMap[$fieldName] = $cm->table['name']; + $this->_quotedTableMap[$cm->table['name']] = $cm->getQuotedTableName($this->_platform); + } + + return $this->_owningTableMap[$fieldName]; + } + + /** + * {@inheritdoc} + */ + public function executeInserts() + { + if ( ! $this->_queuedInserts) { + return; + } + + if ($this->_class->isVersioned) { + $versionedClass = $this->_getVersionedClassMetadata(); + } + + $postInsertIds = array(); + $idGen = $this->_class->idGenerator; + $isPostInsertId = $idGen->isPostInsertGenerator(); + + // Prepare statement for the root table + $rootClass = $this->_class->name == $this->_class->rootEntityName ? + $this->_class : $this->_em->getClassMetadata($this->_class->rootEntityName); + $rootPersister = $this->_em->getUnitOfWork()->getEntityPersister($rootClass->name); + $rootTableName = $rootClass->table['name']; + $rootTableStmt = $this->_conn->prepare($rootPersister->_getInsertSQL()); + + // Prepare statements for sub tables. + $subTableStmts = array(); + if ($rootClass !== $this->_class) { + $subTableStmts[$this->_class->table['name']] = $this->_conn->prepare($this->_getInsertSQL()); + } + foreach ($this->_class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $parentTableName = $parentClass->table['name']; + if ($parentClass !== $rootClass) { + $parentPersister = $this->_em->getUnitOfWork()->getEntityPersister($parentClassName); + $subTableStmts[$parentTableName] = $this->_conn->prepare($parentPersister->_getInsertSQL()); + } + } + + // Execute all inserts. For each entity: + // 1) Insert on root table + // 2) Insert on sub tables + foreach ($this->_queuedInserts as $entity) { + $insertData = $this->_prepareInsertData($entity); + + // Execute insert on root table + $paramIndex = 1; + foreach ($insertData[$rootTableName] as $columnName => $value) { + $rootTableStmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]); + } + $rootTableStmt->execute(); + + if ($isPostInsertId) { + $id = $idGen->generate($this->_em, $entity); + $postInsertIds[$id] = $entity; + } else { + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + } + + // Execute inserts on subtables. + // The order doesn't matter because all child tables link to the root table via FK. + foreach ($subTableStmts as $tableName => $stmt) { + $data = isset($insertData[$tableName]) ? $insertData[$tableName] : array(); + $paramIndex = 1; + foreach ((array) $id as $idVal) { + $stmt->bindValue($paramIndex++, $idVal); + } + foreach ($data as $columnName => $value) { + $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]); + } + $stmt->execute(); + } + } + + $rootTableStmt->closeCursor(); + foreach ($subTableStmts as $stmt) { + $stmt->closeCursor(); + } + + if (isset($versionedClass)) { + $this->_assignDefaultVersionValue($versionedClass, $entity, $id); + } + + $this->_queuedInserts = array(); + + return $postInsertIds; + } + + /** + * {@inheritdoc} + */ + public function update($entity) + { + $updateData = $this->_prepareUpdateData($entity); + + if ($isVersioned = $this->_class->isVersioned) { + $versionedClass = $this->_getVersionedClassMetadata(); + $versionedTable = $versionedClass->table['name']; + } + + if ($updateData) { + foreach ($updateData as $tableName => $data) { + $this->_updateTable($entity, $this->_quotedTableMap[$tableName], $data, $isVersioned && $versionedTable == $tableName); + } + // Make sure the table with the version column is updated even if no columns on that + // table were affected. + if ($isVersioned && ! isset($updateData[$versionedTable])) { + $this->_updateTable($entity, $versionedClass->getQuotedTableName($this->_platform), array(), true); + + $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + $this->_assignDefaultVersionValue($this->_class, $entity, $id); + } + } + } + + /** + * {@inheritdoc} + */ + public function delete($entity) + { + $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity); + $this->deleteJoinTableRecords($identifier); + + $id = array_combine($this->_class->getIdentifierColumnNames(), $identifier); + + // If the database platform supports FKs, just + // delete the row from the root table. Cascades do the rest. + if ($this->_platform->supportsForeignKeyConstraints()) { + $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName) + ->getQuotedTableName($this->_platform), $id); + } else { + // Delete from all tables individually, starting from this class' table up to the root table. + $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id); + foreach ($this->_class->parentClasses as $parentClass) { + $this->_conn->delete($this->_em->getClassMetadata($parentClass)->getQuotedTableName($this->_platform), $id); + } + } + } + + /** + * {@inheritdoc} + */ + protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0) + { + $idColumns = $this->_class->getIdentifierColumnNames(); + $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); + + // Create the column list fragment only once + if ($this->_selectColumnListSql === null) { + // Add regular columns + $columnList = ''; + foreach ($this->_class->fieldMappings as $fieldName => $mapping) { + if ($columnList != '') $columnList .= ', '; + $columnList .= $this->_getSelectColumnSQL($fieldName, + isset($mapping['inherited']) ? + $this->_em->getClassMetadata($mapping['inherited']) : + $this->_class); + } + + // Add foreign key columns + foreach ($this->_class->associationMappings as $assoc2) { + if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE) { + $tableAlias = isset($assoc2['inherited']) ? + $this->_getSQLTableAlias($assoc2['inherited']) + : $baseTableAlias; + foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $columnList .= ", $tableAlias.$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + if ( ! isset($this->_resultColumnNames[$resultColumnName])) { + $this->_resultColumnNames[$resultColumnName] = $srcColumn; + } + } + } + } + + // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#_processSQLResult). + $discrColumn = $this->_class->discriminatorColumn['name']; + if ($this->_class->rootEntityName == $this->_class->name) { + $columnList .= ", $baseTableAlias.$discrColumn"; + } else { + $columnList .= ', ' . $this->_getSQLTableAlias($this->_class->rootEntityName) + . ".$discrColumn"; + } + + $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); + $this->_resultColumnNames[$resultColumnName] = $discrColumn; + } + + // INNER JOIN parent tables + $joinSql = ''; + foreach ($this->_class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $tableAlias = $this->_getSQLTableAlias($parentClassName); + $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($idColumns as $idColumn) { + if ($first) $first = false; else $joinSql .= ' AND '; + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + } + + // OUTER JOIN sub tables + foreach ($this->_class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $this->_getSQLTableAlias($subClassName); + + if ($this->_selectColumnListSql === null) { + // Add subclass columns + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping['inherited'])) { + continue; + } + $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass); + } + + // Add join columns (foreign keys) + foreach ($subClass->associationMappings as $assoc2) { + if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE + && ! isset($assoc2['inherited'])) { + foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $columnList .= ', ' . $tableAlias . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + if ( ! isset($this->_resultColumnNames[$resultColumnName])) { + $this->_resultColumnNames[$resultColumnName] = $srcColumn; + } + } + } + } + } + + // Add LEFT JOIN + $joinSql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($idColumns as $idColumn) { + if ($first) $first = false; else $joinSql .= ' AND '; + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + } + + $joinSql .= $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ? + $this->_getSelectManyToManyJoinSQL($assoc) : ''; + + $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); + + $orderBySql = ''; + if ($assoc != null && isset($assoc['orderBy'])) { + $orderBySql = $this->_getCollectionOrderBySQL($assoc['orderBy'], $baseTableAlias); + } + + if ($this->_selectColumnListSql === null) { + $this->_selectColumnListSql = $columnList; + } + + return 'SELECT ' . $this->_selectColumnListSql + . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias + . $joinSql + . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql; + } + + /** + * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister. + * + * @return string + */ + public function getLockTablesSql() + { + $baseTableAlias = $this->_getSQLTableAlias($this->_class->name); + + // INNER JOIN parent tables + $joinSql = ''; + foreach ($this->_class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $tableAlias = $this->_getSQLTableAlias($parentClassName); + $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($idColumns as $idColumn) { + if ($first) $first = false; else $joinSql .= ' AND '; + $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + } + } + + return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias . $joinSql; + } + + /* Ensure this method is never called. This persister overrides _getSelectEntitiesSQL directly. */ + protected function _getSelectColumnListSQL() + { + throw new \BadMethodCallException("Illegal invocation of ".__METHOD__."."); + } + + /** {@inheritdoc} */ + protected function _getInsertColumnList() + { + // Identifier columns must always come first in the column list of subclasses. + $columns = $this->_class->parentClasses ? $this->_class->getIdentifierColumnNames() : array(); + + foreach ($this->_class->reflFields as $name => $field) { + if (isset($this->_class->fieldMappings[$name]['inherited']) && ! isset($this->_class->fieldMappings[$name]['id']) + || isset($this->_class->associationMappings[$name]['inherited']) + || ($this->_class->isVersioned && $this->_class->versionField == $name)) { + continue; + } + + if (isset($this->_class->associationMappings[$name])) { + $assoc = $this->_class->associationMappings[$name]; + if ($assoc['type'] & ClassMetadata::TO_ONE && $assoc['isOwningSide']) { + foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { + $columns[] = $sourceCol; + } + } + } else if ($this->_class->name != $this->_class->rootEntityName || + ! $this->_class->isIdGeneratorIdentity() || $this->_class->identifier[0] != $name) { + $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform); + } + } + + // Add discriminator column if it is the topmost class. + if ($this->_class->name == $this->_class->rootEntityName) { + $columns[] = $this->_class->discriminatorColumn['name']; + } + + return $columns; + } +} diff --git a/Doctrine/ORM/Persisters/ManyToManyPersister.php b/Doctrine/ORM/Persisters/ManyToManyPersister.php new file mode 100644 index 0000000..27e9a57 --- /dev/null +++ b/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -0,0 +1,176 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; + +/** + * Persister for many-to-many collections. + * + * @author Roman Borschel + * @since 2.0 + */ +class ManyToManyPersister extends AbstractCollectionPersister +{ + /** + * {@inheritdoc} + * + * @override + */ + protected function _getDeleteRowSQL(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $joinTable = $mapping['joinTable']; + $columns = $mapping['joinTableColumns']; + return 'DELETE FROM ' . $joinTable['name'] . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; + } + + /** + * {@inheritdoc} + * + * @override + * @internal Order of the parameters must be the same as the order of the columns in + * _getDeleteRowSql. + */ + protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element) + { + return $this->_collectJoinTableColumnParameters($coll, $element); + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getUpdateRowSQL(PersistentCollection $coll) + {} + + /** + * {@inheritdoc} + * + * @override + * @internal Order of the parameters must be the same as the order of the columns in + * _getInsertRowSql. + */ + protected function _getInsertRowSQL(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $joinTable = $mapping['joinTable']; + $columns = $mapping['joinTableColumns']; + return 'INSERT INTO ' . $joinTable['name'] . ' (' . implode(', ', $columns) . ')' + . ' VALUES (' . implode(', ', array_fill(0, count($columns), '?')) . ')'; + } + + /** + * {@inheritdoc} + * + * @override + * @internal Order of the parameters must be the same as the order of the columns in + * _getInsertRowSql. + */ + protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element) + { + return $this->_collectJoinTableColumnParameters($coll, $element); + } + + /** + * Collects the parameters for inserting/deleting on the join table in the order + * of the join table columns as specified in ManyToManyMapping#joinTableColumns. + * + * @param $coll + * @param $element + * @return array + */ + private function _collectJoinTableColumnParameters(PersistentCollection $coll, $element) + { + $params = array(); + $mapping = $coll->getMapping(); + $isComposite = count($mapping['joinTableColumns']) > 2; + + $identifier1 = $this->_uow->getEntityIdentifier($coll->getOwner()); + $identifier2 = $this->_uow->getEntityIdentifier($element); + + if ($isComposite) { + $class1 = $this->_em->getClassMetadata(get_class($coll->getOwner())); + $class2 = $coll->getTypeClass(); + } + + foreach ($mapping['joinTableColumns'] as $joinTableColumn) { + if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) { + if ($isComposite) { + $params[] = $identifier1[$class1->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; + } else { + $params[] = array_pop($identifier1); + } + } else { + if ($isComposite) { + $params[] = $identifier2[$class2->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + } else { + $params[] = array_pop($identifier2); + } + } + } + + return $params; + } + + /** + * {@inheritdoc} + * + * @override + */ + protected function _getDeleteSQL(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $joinTable = $mapping['joinTable']; + $whereClause = ''; + foreach ($mapping['relationToSourceKeyColumns'] as $relationColumn => $srcColumn) { + if ($whereClause !== '') $whereClause .= ' AND '; + $whereClause .= "$relationColumn = ?"; + } + return 'DELETE FROM ' . $joinTable['name'] . ' WHERE ' . $whereClause; + } + + /** + * {@inheritdoc} + * + * @override + * @internal Order of the parameters must be the same as the order of the columns in + * _getDeleteSql. + */ + protected function _getDeleteSQLParameters(PersistentCollection $coll) + { + $params = array(); + $mapping = $coll->getMapping(); + $identifier = $this->_uow->getEntityIdentifier($coll->getOwner()); + if (count($mapping['relationToSourceKeyColumns']) > 1) { + $sourceClass = $this->_em->getClassMetadata(get_class($mapping->getOwner())); + foreach ($mapping['relationToSourceKeyColumns'] as $relColumn => $srcColumn) { + $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]]; + } + } else { + $params[] = array_pop($identifier); + } + + return $params; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/OneToManyPersister.php b/Doctrine/ORM/Persisters/OneToManyPersister.php new file mode 100644 index 0000000..f0d3aea --- /dev/null +++ b/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -0,0 +1,119 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; + +/** + * Persister for one-to-many collections. + * + * IMPORTANT: + * This persister is only used for uni-directional one-to-many mappings on a foreign key + * (which are not yet supported). So currently this persister is not used. + * + * @since 2.0 + * @author Roman Borschel + * @todo Remove + */ +class OneToManyPersister extends AbstractCollectionPersister +{ + /** + * Generates the SQL UPDATE that updates a particular row's foreign + * key to null. + * + * @param PersistentCollection $coll + * @return string + * @override + */ + protected function _getDeleteRowSQL(PersistentCollection $coll) + { + $mapping = $coll->getMapping(); + $targetClass = $this->_em->getClassMetadata($mapping->getTargetEntityName()); + $table = $targetClass->getTableName(); + + $ownerMapping = $targetClass->getAssociationMapping($mapping['mappedBy']); + + $setClause = ''; + foreach ($ownerMapping->sourceToTargetKeyColumns as $sourceCol => $targetCol) { + if ($setClause != '') $setClause .= ', '; + $setClause .= "$sourceCol = NULL"; + } + + $whereClause = ''; + foreach ($targetClass->getIdentifierColumnNames() as $idColumn) { + if ($whereClause != '') $whereClause .= ' AND '; + $whereClause .= "$idColumn = ?"; + } + + return array("UPDATE $table SET $setClause WHERE $whereClause", $this->_uow->getEntityIdentifier($element)); + } + + protected function _getInsertRowSQL(PersistentCollection $coll) + { + return "UPDATE xxx SET foreign_key = yyy WHERE foreign_key = zzz"; + } + + /* Not used for OneToManyPersister */ + protected function _getUpdateRowSQL(PersistentCollection $coll) + { + return; + } + + /** + * Generates the SQL UPDATE that updates all the foreign keys to null. + * + * @param PersistentCollection $coll + */ + protected function _getDeleteSQL(PersistentCollection $coll) + { + + } + + /** + * Gets the SQL parameters for the corresponding SQL statement to delete + * the given collection. + * + * @param PersistentCollection $coll + */ + protected function _getDeleteSQLParameters(PersistentCollection $coll) + {} + + /** + * Gets the SQL parameters for the corresponding SQL statement to insert the given + * element of the given collection into the database. + * + * @param PersistentCollection $coll + * @param mixed $element + */ + protected function _getInsertRowSQLParameters(PersistentCollection $coll, $element) + {} + + /** + * Gets the SQL parameters for the corresponding SQL statement to delete the given + * element from the given collection. + * + * @param PersistentCollection $coll + * @param mixed $element + */ + protected function _getDeleteRowSQLParameters(PersistentCollection $coll, $element) + {} +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/SingleTablePersister.php b/Doctrine/ORM/Persisters/SingleTablePersister.php new file mode 100644 index 0000000..856ff34 --- /dev/null +++ b/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -0,0 +1,118 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\Mapping\ClassMetadata; + +/** + * Persister for entities that participate in a hierarchy mapped with the + * SINGLE_TABLE strategy. + * + * @author Roman Borschel + * @since 2.0 + * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html + */ +class SingleTablePersister extends AbstractEntityInheritancePersister +{ + /** {@inheritdoc} */ + protected function _getDiscriminatorColumnTableName() + { + return $this->_class->table['name']; + } + + /** {@inheritdoc} */ + protected function _getSelectColumnListSQL() + { + $columnList = parent::_getSelectColumnListSQL(); + + // Append discriminator column + $discrColumn = $this->_class->discriminatorColumn['name']; + $columnList .= ", $discrColumn"; + $rootClass = $this->_em->getClassMetadata($this->_class->rootEntityName); + $tableAlias = $this->_getSQLTableAlias($rootClass->name); + $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn); + $this->_resultColumnNames[$resultColumnName] = $discrColumn; + + // Append subclass columns + foreach ($this->_class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + // Regular columns + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if ( ! isset($mapping['inherited'])) { + $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass); + } + } + // Foreign key columns + foreach ($subClass->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE && ! isset($assoc['inherited'])) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $srcColumn . $this->_sqlAliasCounter++; + $columnList .= ', ' . $tableAlias . ".$srcColumn AS $columnAlias"; + $resultColumnName = $this->_platform->getSQLResultCasing($columnAlias); + if ( ! isset($this->_resultColumnNames[$resultColumnName])) { + $this->_resultColumnNames[$resultColumnName] = $srcColumn; + } + } + } + } + } + + return $columnList; + } + + /** {@inheritdoc} */ + protected function _getInsertColumnList() + { + $columns = parent::_getInsertColumnList(); + // Add discriminator column to the INSERT SQL + $columns[] = $this->_class->discriminatorColumn['name']; + + return $columns; + } + + /** {@inheritdoc} */ + protected function _getSQLTableAlias($className) + { + return parent::_getSQLTableAlias($this->_class->rootEntityName); + } + + /** {@inheritdoc} */ + protected function _getSelectConditionSQL(array $criteria, $assoc = null) + { + $conditionSql = parent::_getSelectConditionSQL($criteria, $assoc); + + // Append discriminator condition + if ($conditionSql) $conditionSql .= ' AND '; + $values = array(); + if ($this->_class->discriminatorValue !== null) { // discriminators can be 0 + $values[] = $this->_conn->quote($this->_class->discriminatorValue); + } + + $discrValues = array_flip($this->_class->discriminatorMap); + foreach ($this->_class->subClasses as $subclassName) { + $values[] = $this->_conn->quote($discrValues[$subclassName]); + } + $conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.' + . $this->_class->discriminatorColumn['name'] + . ' IN (' . implode(', ', $values) . ')'; + + return $conditionSql; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Persisters/UnionSubclassPersister.php b/Doctrine/ORM/Persisters/UnionSubclassPersister.php new file mode 100644 index 0000000..b2e683a --- /dev/null +++ b/Doctrine/ORM/Persisters/UnionSubclassPersister.php @@ -0,0 +1,8 @@ +. +*/ + +namespace Doctrine\ORM; + +/** + * Pessimistic Lock Exception + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class PessimisticLockException extends ORMException +{ + public static function lockFailed() + { + return new self("The pessimistic lock failed."); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Proxy/Proxy.php b/Doctrine/ORM/Proxy/Proxy.php new file mode 100644 index 0000000..853f9c1 --- /dev/null +++ b/Doctrine/ORM/Proxy/Proxy.php @@ -0,0 +1,30 @@ +. + */ + +namespace Doctrine\ORM\Proxy; + +/** + * Interface for proxy classes. + * + * @author Roman Borschel + * @since 2.0 + */ +interface Proxy {} \ No newline at end of file diff --git a/Doctrine/ORM/Proxy/ProxyException.php b/Doctrine/ORM/Proxy/ProxyException.php new file mode 100644 index 0000000..892dbeb --- /dev/null +++ b/Doctrine/ORM/Proxy/ProxyException.php @@ -0,0 +1,43 @@ +. +*/ + +namespace Doctrine\ORM\Proxy; + +/** + * ORM Proxy Exception + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class ProxyException extends \Doctrine\ORM\ORMException { + + public static function proxyDirectoryRequired() { + return new self("You must configure a proxy directory. See docs for details"); + } + + public static function proxyNamespaceRequired() { + return new self("You must configure a proxy namespace. See docs for details"); + } + +} \ No newline at end of file diff --git a/Doctrine/ORM/Proxy/ProxyFactory.php b/Doctrine/ORM/Proxy/ProxyFactory.php new file mode 100644 index 0000000..8be75d9 --- /dev/null +++ b/Doctrine/ORM/Proxy/ProxyFactory.php @@ -0,0 +1,289 @@ +. + */ + +namespace Doctrine\ORM\Proxy; + +use Doctrine\ORM\EntityManager, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\Mapping\AssociationMapping; + +/** + * This factory is used to create proxy objects for entities at runtime. + * + * @author Roman Borschel + * @author Giorgio Sironi + * @since 2.0 + */ +class ProxyFactory +{ + /** The EntityManager this factory is bound to. */ + private $_em; + /** Whether to automatically (re)generate proxy classes. */ + private $_autoGenerate; + /** The namespace that contains all proxy classes. */ + private $_proxyNamespace; + /** The directory that contains all proxy classes. */ + private $_proxyDir; + + /** + * Initializes a new instance of the ProxyFactory class that is + * connected to the given EntityManager. + * + * @param EntityManager $em The EntityManager the new factory works for. + * @param string $proxyDir The directory to use for the proxy classes. It must exist. + * @param string $proxyNs The namespace to use for the proxy classes. + * @param boolean $autoGenerate Whether to automatically generate proxy classes. + */ + public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = false) + { + if ( ! $proxyDir) { + throw ProxyException::proxyDirectoryRequired(); + } + if ( ! $proxyNs) { + throw ProxyException::proxyNamespaceRequired(); + } + $this->_em = $em; + $this->_proxyDir = $proxyDir; + $this->_autoGenerate = $autoGenerate; + $this->_proxyNamespace = $proxyNs; + } + + /** + * Gets a reference proxy instance for the entity of the given type and identified by + * the given identifier. + * + * @param string $className + * @param mixed $identifier + * @return object + */ + public function getProxy($className, $identifier) + { + $proxyClassName = str_replace('\\', '', $className) . 'Proxy'; + $fqn = $this->_proxyNamespace . '\\' . $proxyClassName; + + if (! class_exists($fqn, false)) { + $fileName = $this->_proxyDir . DIRECTORY_SEPARATOR . $proxyClassName . '.php'; + if ($this->_autoGenerate) { + $this->_generateProxyClass($this->_em->getClassMetadata($className), $proxyClassName, $fileName, self::$_proxyClassTemplate); + } + require $fileName; + } + + if ( ! $this->_em->getMetadataFactory()->hasMetadataFor($fqn)) { + $this->_em->getMetadataFactory()->setMetadataFor($fqn, $this->_em->getClassMetadata($className)); + } + + $entityPersister = $this->_em->getUnitOfWork()->getEntityPersister($className); + + return new $fqn($entityPersister, $identifier); + } + + /** + * Generates proxy classes for all given classes. + * + * @param array $classes The classes (ClassMetadata instances) for which to generate proxies. + * @param string $toDir The target directory of the proxy classes. If not specified, the + * directory configured on the Configuration of the EntityManager used + * by this factory is used. + */ + public function generateProxyClasses(array $classes, $toDir = null) + { + $proxyDir = $toDir ?: $this->_proxyDir; + $proxyDir = rtrim($proxyDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; + foreach ($classes as $class) { + /* @var $class ClassMetadata */ + if ($class->isMappedSuperclass) { + continue; + } + + $proxyClassName = str_replace('\\', '', $class->name) . 'Proxy'; + $proxyFileName = $proxyDir . $proxyClassName . '.php'; + $this->_generateProxyClass($class, $proxyClassName, $proxyFileName, self::$_proxyClassTemplate); + } + } + + /** + * Generates a proxy class file. + * + * @param $class + * @param $originalClassName + * @param $proxyClassName + * @param $file The path of the file to write to. + */ + private function _generateProxyClass($class, $proxyClassName, $fileName, $file) + { + $methods = $this->_generateMethods($class); + $sleepImpl = $this->_generateSleep($class); + + $placeholders = array( + '', + '', '', + '', '' + ); + + if(substr($class->name, 0, 1) == "\\") { + $className = substr($class->name, 1); + } else { + $className = $class->name; + } + + $replacements = array( + $this->_proxyNamespace, + $proxyClassName, $className, + $methods, $sleepImpl + ); + + $file = str_replace($placeholders, $replacements, $file); + + file_put_contents($fileName, $file, LOCK_EX); + } + + /** + * Generates the methods of a proxy class. + * + * @param ClassMetadata $class + * @return string The code of the generated methods. + */ + private function _generateMethods(ClassMetadata $class) + { + $methods = ''; + + foreach ($class->reflClass->getMethods() as $method) { + /* @var $method ReflectionMethod */ + if ($method->isConstructor() || strtolower($method->getName()) == "__sleep") { + continue; + } + + if ($method->isPublic() && ! $method->isFinal() && ! $method->isStatic()) { + $methods .= PHP_EOL . ' public function '; + if ($method->returnsReference()) { + $methods .= '&'; + } + $methods .= $method->getName() . '('; + $firstParam = true; + $parameterString = $argumentString = ''; + + foreach ($method->getParameters() as $param) { + if ($firstParam) { + $firstParam = false; + } else { + $parameterString .= ', '; + $argumentString .= ', '; + } + + // We need to pick the type hint class too + if (($paramClass = $param->getClass()) !== null) { + $parameterString .= '\\' . $paramClass->getName() . ' '; + } else if ($param->isArray()) { + $parameterString .= 'array '; + } + + if ($param->isPassedByReference()) { + $parameterString .= '&'; + } + + $parameterString .= '$' . $param->getName(); + $argumentString .= '$' . $param->getName(); + + if ($param->isDefaultValueAvailable()) { + $parameterString .= ' = ' . var_export($param->getDefaultValue(), true); + } + } + + $methods .= $parameterString . ')'; + $methods .= PHP_EOL . ' {' . PHP_EOL; + $methods .= ' $this->_load();' . PHP_EOL; + $methods .= ' return parent::' . $method->getName() . '(' . $argumentString . ');'; + $methods .= PHP_EOL . ' }' . PHP_EOL; + } + } + + return $methods; + } + + /** + * Generates the code for the __sleep method for a proxy class. + * + * @param $class + * @return string + */ + private function _generateSleep(ClassMetadata $class) + { + $sleepImpl = ''; + + if ($class->reflClass->hasMethod('__sleep')) { + $sleepImpl .= "return array_merge(array('__isInitialized__'), parent::__sleep());"; + } else { + $sleepImpl .= "return array('__isInitialized__', "; + $first = true; + + foreach ($class->getReflectionProperties() as $name => $prop) { + if ($first) { + $first = false; + } else { + $sleepImpl .= ', '; + } + + $sleepImpl .= "'" . $name . "'"; + } + + $sleepImpl .= ');'; + } + + return $sleepImpl; + } + + /** Proxy class code template */ + private static $_proxyClassTemplate = +'; + +/** + * THIS CLASS WAS GENERATED BY THE DOCTRINE ORM. DO NOT EDIT THIS FILE. + */ +class extends \ implements \Doctrine\ORM\Proxy\Proxy +{ + private $_entityPersister; + private $_identifier; + public $__isInitialized__ = false; + public function __construct($entityPersister, $identifier) + { + $this->_entityPersister = $entityPersister; + $this->_identifier = $identifier; + } + private function _load() + { + if (!$this->__isInitialized__ && $this->_entityPersister) { + $this->__isInitialized__ = true; + if ($this->_entityPersister->load($this->_identifier, $this) === null) { + throw new \Doctrine\ORM\EntityNotFoundException(); + } + unset($this->_entityPersister, $this->_identifier); + } + } + + + + public function __sleep() + { + + } +}'; +} diff --git a/Doctrine/ORM/Query.php b/Doctrine/ORM/Query.php new file mode 100644 index 0000000..d428e2d --- /dev/null +++ b/Doctrine/ORM/Query.php @@ -0,0 +1,562 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\DBAL\LockMode, + Doctrine\ORM\Query\Parser, + Doctrine\ORM\Query\QueryException; + +/** + * A Query object represents a DQL query. + * + * @since 1.0 + * @author Guilherme Blanco + * @author Konsta Vesterinen + * @author Roman Borschel + */ +final class Query extends AbstractQuery +{ + /* Query STATES */ + /** + * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts. + */ + const STATE_CLEAN = 1; + /** + * A query object is in state DIRTY when it has DQL parts that have not yet been + * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart + * is called. + */ + const STATE_DIRTY = 2; + + /* Query HINTS */ + /** + * The refresh hint turns any query into a refresh query with the result that + * any local changes in entities are overridden with the fetched values. + * + * @var string + */ + const HINT_REFRESH = 'doctrine.refresh'; + /** + * The forcePartialLoad query hint forces a particular query to return + * partial objects. + * + * @var string + * @todo Rename: HINT_OPTIMIZE + */ + const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad'; + /** + * The includeMetaColumns query hint causes meta columns like foreign keys and + * discriminator columns to be selected and returned as part of the query result. + * + * This hint does only apply to non-object queries. + * + * @var string + */ + const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns'; + + /** + * An array of class names that implement Doctrine\ORM\Query\TreeWalker and + * are iterated and executed after the DQL has been parsed into an AST. + * + * @var string + */ + const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers'; + + /** + * A string with a class name that implements Doctrine\ORM\Query\TreeWalker + * and is used for generating the target SQL from any DQL AST tree. + * + * @var string + */ + const HINT_CUSTOM_OUTPUT_WALKER = 'doctrine.customOutputWalker'; + + //const HINT_READ_ONLY = 'doctrine.readOnly'; + + /** + * @var string + */ + const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration'; + + /** + * @var string + */ + const HINT_LOCK_MODE = 'doctrine.lockMode'; + + /** + * @var integer $_state The current state of this query. + */ + private $_state = self::STATE_CLEAN; + + /** + * @var string $_dql Cached DQL query. + */ + private $_dql = null; + + /** + * @var Doctrine\ORM\Query\ParserResult The parser result that holds DQL => SQL information. + */ + private $_parserResult; + + /** + * @var integer The first result to return (the "offset"). + */ + private $_firstResult = null; + + /** + * @var integer The maximum number of results to return (the "limit"). + */ + private $_maxResults = null; + + /** + * @var CacheDriver The cache driver used for caching queries. + */ + private $_queryCache; + + /** + * @var boolean Boolean value that indicates whether or not expire the query cache. + */ + private $_expireQueryCache = false; + + /** + * @var int Query Cache lifetime. + */ + private $_queryCacheTTL; + + /** + * @var boolean Whether to use a query cache, if available. Defaults to TRUE. + */ + private $_useQueryCache = true; + + // End of Caching Stuff + + /** + * Initializes a new Query instance. + * + * @param Doctrine\ORM\EntityManager $entityManager + */ + /*public function __construct(EntityManager $entityManager) + { + parent::__construct($entityManager); + }*/ + + /** + * Gets the SQL query/queries that correspond to this DQL query. + * + * @return mixed The built sql query or an array of all sql queries. + * @override + */ + public function getSQL() + { + return $this->_parse()->getSQLExecutor()->getSQLStatements(); + } + + /** + * Returns the corresponding AST for this DQL query. + * + * @return Doctrine\ORM\Query\AST\SelectStatement | + * Doctrine\ORM\Query\AST\UpdateStatement | + * Doctrine\ORM\Query\AST\DeleteStatement + */ + public function getAST() + { + $parser = new Parser($this); + return $parser->getAST(); + } + + /** + * Parses the DQL query, if necessary, and stores the parser result. + * + * Note: Populates $this->_parserResult as a side-effect. + * + * @return Doctrine\ORM\Query\ParserResult + */ + private function _parse() + { + if ($this->_state === self::STATE_CLEAN) { + return $this->_parserResult; + } + + // Check query cache. + if ($this->_useQueryCache && ($queryCache = $this->getQueryCacheDriver())) { + $hash = $this->_getQueryCacheId(); + $cached = $this->_expireQueryCache ? false : $queryCache->fetch($hash); + if ($cached === false) { + // Cache miss. + $parser = new Parser($this); + $this->_parserResult = $parser->parse(); + $queryCache->save($hash, $this->_parserResult, $this->_queryCacheTTL); + } else { + // Cache hit. + $this->_parserResult = $cached; + } + } else { + $parser = new Parser($this); + $this->_parserResult = $parser->parse(); + } + $this->_state = self::STATE_CLEAN; + + return $this->_parserResult; + } + + /** + * {@inheritdoc} + */ + protected function _doExecute() + { + $executor = $this->_parse()->getSqlExecutor(); + + // Prepare parameters + $paramMappings = $this->_parserResult->getParameterMappings(); + + if (count($paramMappings) != count($this->_params)) { + throw QueryException::invalidParameterNumber(); + } + + $sqlParams = $types = array(); + + foreach ($this->_params as $key => $value) { + if ( ! isset($paramMappings[$key])) { + throw QueryException::unknownParameter($key); + } + if (isset($this->_paramTypes[$key])) { + foreach ($paramMappings[$key] as $position) { + $types[$position] = $this->_paramTypes[$key]; + } + } + + if (is_object($value) && $this->_em->getMetadataFactory()->hasMetadataFor(get_class($value))) { + if ($this->_em->getUnitOfWork()->getEntityState($value) == UnitOfWork::STATE_MANAGED) { + $idValues = $this->_em->getUnitOfWork()->getEntityIdentifier($value); + } else { + $class = $this->_em->getClassMetadata(get_class($value)); + $idValues = $class->getIdentifierValues($value); + } + $sqlPositions = $paramMappings[$key]; + $sqlParams += array_combine((array)$sqlPositions, $idValues); + } else { + foreach ($paramMappings[$key] as $position) { + $sqlParams[$position] = $value; + } + } + } + + if ($sqlParams) { + ksort($sqlParams); + $sqlParams = array_values($sqlParams); + } + + if ($this->_resultSetMapping === null) { + $this->_resultSetMapping = $this->_parserResult->getResultSetMapping(); + } + + return $executor->execute($this->_em->getConnection(), $sqlParams, $types); + } + + /** + * Defines a cache driver to be used for caching queries. + * + * @param Doctrine_Cache_Interface|null $driver Cache driver + * @return Query This query instance. + */ + public function setQueryCacheDriver($queryCache) + { + $this->_queryCache = $queryCache; + return $this; + } + + /** + * Defines whether the query should make use of a query cache, if available. + * + * @param boolean $bool + * @return @return Query This query instance. + */ + public function useQueryCache($bool) + { + $this->_useQueryCache = $bool; + return $this; + } + + /** + * Returns the cache driver used for query caching. + * + * @return CacheDriver The cache driver used for query caching or NULL, if this + * Query does not use query caching. + */ + public function getQueryCacheDriver() + { + if ($this->_queryCache) { + return $this->_queryCache; + } else { + return $this->_em->getConfiguration()->getQueryCacheImpl(); + } + } + + /** + * Defines how long the query cache will be active before expire. + * + * @param integer $timeToLive How long the cache entry is valid + * @return Query This query instance. + */ + public function setQueryCacheLifetime($timeToLive) + { + if ($timeToLive !== null) { + $timeToLive = (int) $timeToLive; + } + $this->_queryCacheTTL = $timeToLive; + + return $this; + } + + /** + * Retrieves the lifetime of resultset cache. + * + * @return int + */ + public function getQueryCacheLifetime() + { + return $this->_queryCacheTTL; + } + + /** + * Defines if the query cache is active or not. + * + * @param boolean $expire Whether or not to force query cache expiration. + * @return Query This query instance. + */ + public function expireQueryCache($expire = true) + { + $this->_expireQueryCache = $expire; + + return $this; + } + + /** + * Retrieves if the query cache is active or not. + * + * @return bool + */ + public function getExpireQueryCache() + { + return $this->_expireQueryCache; + } + + /** + * @override + */ + public function free() + { + parent::free(); + $this->_dql = null; + $this->_state = self::STATE_CLEAN; + } + + /** + * Sets a DQL query string. + * + * @param string $dqlQuery DQL Query + * @return Doctrine\ORM\AbstractQuery + */ + public function setDQL($dqlQuery) + { + if ($dqlQuery !== null) { + $this->_dql = $dqlQuery; + $this->_state = self::STATE_DIRTY; + } + return $this; + } + + /** + * Returns the DQL query that is represented by this query object. + * + * @return string DQL query + */ + public function getDQL() + { + return $this->_dql; + } + + /** + * Returns the state of this query object + * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL + * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY. + * + * @see AbstractQuery::STATE_CLEAN + * @see AbstractQuery::STATE_DIRTY + * + * @return integer Return the query state + */ + public function getState() + { + return $this->_state; + } + + /** + * Method to check if an arbitrary piece of DQL exists + * + * @param string $dql Arbitrary piece of DQL to check for + * @return boolean + */ + public function contains($dql) + { + return stripos($this->getDQL(), $dql) === false ? false : true; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param integer $firstResult The first result to return. + * @return Query This query object. + */ + public function setFirstResult($firstResult) + { + $this->_firstResult = $firstResult; + $this->_state = self::STATE_DIRTY; + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this query. + * + * @return integer The position of the first result. + */ + public function getFirstResult() + { + return $this->_firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param integer $maxResults + * @return Query This query object. + */ + public function setMaxResults($maxResults) + { + $this->_maxResults = $maxResults; + $this->_state = self::STATE_DIRTY; + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query. + * + * @return integer Maximum number of results. + */ + public function getMaxResults() + { + return $this->_maxResults; + } + + /** + * Executes the query and returns an IterableResult that can be used to incrementally + * iterated over the result. + * + * @param array $params The query parameters. + * @param integer $hydrationMode The hydration mode to use. + * @return IterableResult + */ + public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT) + { + $this->setHint(self::HINT_INTERNAL_ITERATION, true); + return parent::iterate($params, $hydrationMode); + } + + /** + * {@inheritdoc} + */ + public function setHint($name, $value) + { + $this->_state = self::STATE_DIRTY; + return parent::setHint($name, $value); + } + + /** + * {@inheritdoc} + */ + public function setHydrationMode($hydrationMode) + { + $this->_state = self::STATE_DIRTY; + return parent::setHydrationMode($hydrationMode); + } + + /** + * Set the lock mode for this Query. + * + * @see Doctrine\DBAL\LockMode + * @param int $lockMode + * @return Query + */ + public function setLockMode($lockMode) + { + if ($lockMode == LockMode::PESSIMISTIC_READ || $lockMode == LockMode::PESSIMISTIC_WRITE) { + if (!$this->_em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + } + + $this->setHint(self::HINT_LOCK_MODE, $lockMode); + return $this; + } + + /** + * Get the current lock mode for this query. + * + * @return int + */ + public function getLockMode() + { + $lockMode = $this->getHint(self::HINT_LOCK_MODE); + if (!$lockMode) { + return LockMode::NONE; + } + return $lockMode; + } + + /** + * Generate a cache id for the query cache - reusing the Result-Cache-Id generator. + * + * The query cache + * + * @return string + */ + protected function _getQueryCacheId() + { + ksort($this->_hints); + + return md5( + $this->getDql() . var_export($this->_hints, true) . + '&firstResult=' . $this->_firstResult . '&maxResult=' . $this->_maxResults . + '&hydrationMode='.$this->_hydrationMode.'DOCTRINE_QUERY_CACHE_SALT' + ); + } + + /** + * Cleanup Query resource when clone is called. + * + * @return void + */ + public function __clone() + { + parent::__clone(); + $this->_state = self::STATE_DIRTY; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ASTException.php b/Doctrine/ORM/Query/AST/ASTException.php new file mode 100644 index 0000000..7472572 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ASTException.php @@ -0,0 +1,13 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Description of AggregateExpression + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class AggregateExpression extends Node +{ + public $functionName; + public $pathExpression; + public $isDistinct = false; // Some aggregate expressions support distinct, eg COUNT + + public function __construct($functionName, $pathExpression, $isDistinct) + { + $this->functionName = $functionName; + $this->pathExpression = $pathExpression; + $this->isDistinct = $isDistinct; + } + + public function dispatch($walker) + { + return $walker->walkAggregateExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ArithmeticExpression.php b/Doctrine/ORM/Query/AST/ArithmeticExpression.php new file mode 100644 index 0000000..679cbcc --- /dev/null +++ b/Doctrine/ORM/Query/AST/ArithmeticExpression.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ArithmeticExpression extends Node +{ + public $simpleArithmeticExpression; + public $subselect; + + public function isSimpleArithmeticExpression() + { + return (bool) $this->simpleArithmeticExpression; + } + + public function isSubselect() + { + return (bool) $this->subselect; + } + + public function dispatch($walker) + { + return $walker->walkArithmeticExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ArithmeticFactor.php b/Doctrine/ORM/Query/AST/ArithmeticFactor.php new file mode 100644 index 0000000..b559e4a --- /dev/null +++ b/Doctrine/ORM/Query/AST/ArithmeticFactor.php @@ -0,0 +1,67 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ArithmeticFactor extends Node +{ + /** + * @var ArithmeticPrimary + */ + public $arithmeticPrimary; + + /** + * @var null|boolean NULL represents no sign, TRUE means positive and FALSE means negative sign + */ + public $sign; + + public function __construct($arithmeticPrimary, $sign = null) + { + $this->arithmeticPrimary = $arithmeticPrimary; + $this->sign = $sign; + } + + public function isPositiveSigned() + { + return $this->sign === true; + } + + public function isNegativeSigned() + { + return $this->sign === false; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkArithmeticFactor($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ArithmeticTerm.php b/Doctrine/ORM/Query/AST/ArithmeticTerm.php new file mode 100644 index 0000000..a233e05 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ArithmeticTerm.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ArithmeticTerm extends Node +{ + public $arithmeticFactors; + + public function __construct(array $arithmeticFactors) + { + $this->arithmeticFactors = $arithmeticFactors; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkArithmeticTerm($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/BetweenExpression.php b/Doctrine/ORM/Query/AST/BetweenExpression.php new file mode 100644 index 0000000..c00bca4 --- /dev/null +++ b/Doctrine/ORM/Query/AST/BetweenExpression.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Description of BetweenExpression + * + @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class BetweenExpression extends Node +{ + public $expression; + public $leftBetweenExpression; + public $rightBetweenExpression; + public $not; + + public function __construct($expr, $leftExpr, $rightExpr) + { + $this->expression = $expr; + $this->leftBetweenExpression = $leftExpr; + $this->rightBetweenExpression = $rightExpr; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkBetweenExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/CollectionMemberExpression.php b/Doctrine/ORM/Query/AST/CollectionMemberExpression.php new file mode 100644 index 0000000..ea252de --- /dev/null +++ b/Doctrine/ORM/Query/AST/CollectionMemberExpression.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class CollectionMemberExpression extends Node +{ + public $entityExpression; + public $collectionValuedPathExpression; + public $not; + + public function __construct($entityExpr, $collValuedPathExpr) + { + $this->entityExpression = $entityExpr; + $this->collectionValuedPathExpression = $collValuedPathExpr; + } + + public function dispatch($walker) + { + return $walker->walkCollectionMemberExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/ComparisonExpression.php b/Doctrine/ORM/Query/AST/ComparisonExpression.php new file mode 100644 index 0000000..a13ae26 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ComparisonExpression.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | + * StringExpression ComparisonOperator (StringExpression | QuantifiedExpression) | + * BooleanExpression ("=" | "<>" | "!=") (BooleanExpression | QuantifiedExpression) | + * EnumExpression ("=" | "<>" | "!=") (EnumExpression | QuantifiedExpression) | + * DatetimeExpression ComparisonOperator (DatetimeExpression | QuantifiedExpression) | + * EntityExpression ("=" | "<>") (EntityExpression | QuantifiedExpression) + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ComparisonExpression extends Node +{ + public $leftExpression; + public $rightExpression; + public $operator; + + public function __construct($leftExpr, $operator, $rightExpr) + { + $this->leftExpression = $leftExpr; + $this->rightExpression = $rightExpr; + $this->operator = $operator; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkComparisonExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ConditionalExpression.php b/Doctrine/ORM/Query/AST/ConditionalExpression.php new file mode 100644 index 0000000..d2e7762 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ConditionalExpression.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConditionalExpression extends Node +{ + public $conditionalTerms = array(); + + public function __construct(array $conditionalTerms) + { + $this->conditionalTerms = $conditionalTerms; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkConditionalExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ConditionalFactor.php b/Doctrine/ORM/Query/AST/ConditionalFactor.php new file mode 100644 index 0000000..f0b165b --- /dev/null +++ b/Doctrine/ORM/Query/AST/ConditionalFactor.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ConditionalFactor ::= ["NOT"] ConditionalPrimary + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConditionalFactor extends Node +{ + public $not = false; + public $conditionalPrimary; + + public function __construct($conditionalPrimary) + { + $this->conditionalPrimary = $conditionalPrimary; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkConditionalFactor($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ConditionalPrimary.php b/Doctrine/ORM/Query/AST/ConditionalPrimary.php new file mode 100644 index 0000000..0e3f127 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ConditionalPrimary.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConditionalPrimary extends Node +{ + public $simpleConditionalExpression; + public $conditionalExpression; + + public function isSimpleConditionalExpression() + { + return (bool) $this->simpleConditionalExpression; + } + + public function isConditionalExpression() + { + return (bool) $this->conditionalExpression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkConditionalPrimary($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/ConditionalTerm.php b/Doctrine/ORM/Query/AST/ConditionalTerm.php new file mode 100644 index 0000000..78ba6a2 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ConditionalTerm.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConditionalTerm extends Node +{ + public $conditionalFactors = array(); + + public function __construct(array $conditionalFactors) + { + $this->conditionalFactors = $conditionalFactors; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkConditionalTerm($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/DeleteClause.php b/Doctrine/ORM/Query/AST/DeleteClause.php new file mode 100644 index 0000000..4acf49c --- /dev/null +++ b/Doctrine/ORM/Query/AST/DeleteClause.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName [["AS"] AliasIdentificationVariable] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DeleteClause extends Node +{ + public $abstractSchemaName; + public $aliasIdentificationVariable; + + public function __construct($abstractSchemaName) + { + $this->abstractSchemaName = $abstractSchemaName; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkDeleteClause($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/DeleteStatement.php b/Doctrine/ORM/Query/AST/DeleteStatement.php new file mode 100644 index 0000000..de807ab --- /dev/null +++ b/Doctrine/ORM/Query/AST/DeleteStatement.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * DeleteStatement = DeleteClause [WhereClause] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DeleteStatement extends Node +{ + public $deleteClause; + public $whereClause; + + public function __construct($deleteClause) + { + $this->deleteClause = $deleteClause; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkDeleteStatement($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/EmptyCollectionComparisonExpression.php b/Doctrine/ORM/Query/AST/EmptyCollectionComparisonExpression.php new file mode 100644 index 0000000..271d304 --- /dev/null +++ b/Doctrine/ORM/Query/AST/EmptyCollectionComparisonExpression.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EmptyCollectionComparisonExpression extends Node +{ + public $expression; + public $not; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkEmptyCollectionComparisonExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/ExistsExpression.php b/Doctrine/ORM/Query/AST/ExistsExpression.php new file mode 100644 index 0000000..3d9ad20 --- /dev/null +++ b/Doctrine/ORM/Query/AST/ExistsExpression.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ExistsExpression extends Node +{ + public $not; + public $subselect; + + public function __construct($subselect) + { + $this->subselect = $subselect; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkExistsExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/FromClause.php b/Doctrine/ORM/Query/AST/FromClause.php new file mode 100644 index 0000000..5325ea7 --- /dev/null +++ b/Doctrine/ORM/Query/AST/FromClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class FromClause extends Node +{ + public $identificationVariableDeclarations = array(); + + public function __construct(array $identificationVariableDeclarations) + { + $this->identificationVariableDeclarations = $identificationVariableDeclarations; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkFromClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Functions/AbsFunction.php b/Doctrine/ORM/Query/AST/Functions/AbsFunction.php new file mode 100644 index 0000000..3fafccd --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/AbsFunction.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "ABS" "(" SimpleArithmeticExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class AbsFunction extends FunctionNode +{ + public $simpleArithmeticExpression; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return 'ABS(' . $sqlWalker->walkSimpleArithmeticExpression( + $this->simpleArithmeticExpression + ) . ')'; + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php b/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php new file mode 100644 index 0000000..bb2333a --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php @@ -0,0 +1,67 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "CONCAT" "(" StringPrimary "," StringPrimary ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class ConcatFunction extends FunctionNode +{ + public $firstStringPrimary; + public $secondStringPriamry; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + return $platform->getConcatExpression( + $sqlWalker->walkStringPrimary($this->firstStringPrimary), + $sqlWalker->walkStringPrimary($this->secondStringPrimary) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->firstStringPrimary = $parser->StringPrimary(); + $parser->match(Lexer::T_COMMA); + $this->secondStringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/CurrentDateFunction.php b/Doctrine/ORM/Query/AST/Functions/CurrentDateFunction.php new file mode 100644 index 0000000..7bf7683 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/CurrentDateFunction.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "CURRENT_DATE" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class CurrentDateFunction extends FunctionNode +{ + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentDateSQL(); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Functions/CurrentTimeFunction.php b/Doctrine/ORM/Query/AST/Functions/CurrentTimeFunction.php new file mode 100644 index 0000000..1335755 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/CurrentTimeFunction.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "CURRENT_TIME" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class CurrentTimeFunction extends FunctionNode +{ + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimeSQL(); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Functions/CurrentTimestampFunction.php b/Doctrine/ORM/Query/AST/Functions/CurrentTimestampFunction.php new file mode 100644 index 0000000..7b1a25d --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/CurrentTimestampFunction.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "CURRENT_TIMESTAMP" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class CurrentTimestampFunction extends FunctionNode +{ + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getCurrentTimestampSQL(); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Functions/FunctionNode.php b/Doctrine/ORM/Query/AST/Functions/FunctionNode.php new file mode 100644 index 0000000..b9d36d1 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/FunctionNode.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\AST\Node; + +/** + * Abtract Function Node. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +abstract class FunctionNode extends Node +{ + public $name; + + public function __construct($name) + { + $this->name = $name; + } + + abstract public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker); + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkFunction($this); + } + + abstract public function parse(\Doctrine\ORM\Query\Parser $parser); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Functions/LengthFunction.php b/Doctrine/ORM/Query/AST/Functions/LengthFunction.php new file mode 100644 index 0000000..3678778 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/LengthFunction.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "LENGTH" "(" StringPrimary ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class LengthFunction extends FunctionNode +{ + public $stringPrimary; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getLengthExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/LocateFunction.php b/Doctrine/ORM/Query/AST/Functions/LocateFunction.php new file mode 100644 index 0000000..a4ea716 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/LocateFunction.php @@ -0,0 +1,81 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class LocateFunction extends FunctionNode +{ + public $firstStringPrimary; + public $secondStringPrimary; + public $simpleArithmeticExpression = false; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + + return $sqlWalker->getConnection()->getDatabasePlatform()->getLocateExpression( + $sqlWalker->walkStringPrimary($this->secondStringPrimary), // its the other way around in platform + $sqlWalker->walkStringPrimary($this->firstStringPrimary), + (($this->simpleArithmeticExpression) + ? $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression) + : false + ) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->firstStringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_COMMA); + + $this->secondStringPrimary = $parser->StringPrimary(); + + $lexer = $parser->getLexer(); + if ($lexer->isNextToken(Lexer::T_COMMA)) { + $parser->match(Lexer::T_COMMA); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + } + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/LowerFunction.php b/Doctrine/ORM/Query/AST/Functions/LowerFunction.php new file mode 100644 index 0000000..775f51d --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/LowerFunction.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "LOWER" "(" StringPrimary ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class LowerFunction extends FunctionNode +{ + public $stringPrimary; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getLowerExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/ModFunction.php b/Doctrine/ORM/Query/AST/Functions/ModFunction.php new file mode 100644 index 0000000..4d124fe --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/ModFunction.php @@ -0,0 +1,68 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class ModFunction extends FunctionNode +{ + public $firstSimpleArithmeticExpression; + public $secondSimpleArithmeticExpression; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getModExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), + $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_MOD); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(Lexer::T_COMMA); + + $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/SizeFunction.php b/Doctrine/ORM/Query/AST/Functions/SizeFunction.php new file mode 100644 index 0000000..601c4f8 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/SizeFunction.php @@ -0,0 +1,122 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "SIZE" "(" CollectionValuedPathExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class SizeFunction extends FunctionNode +{ + public $collectionPathExpression; + + /** + * @override + * @todo If the collection being counted is already joined, the SQL can be simpler (more efficient). + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + $dqlAlias = $this->collectionPathExpression->identificationVariable; + $assocField = $this->collectionPathExpression->field; + + $qComp = $sqlWalker->getQueryComponent($dqlAlias); + $class = $qComp['metadata']; + $assoc = $class->associationMappings[$assocField]; + $sql = 'SELECT COUNT(*) FROM '; + + if ($assoc['type'] == \Doctrine\ORM\Mapping\ClassMetadata::ONE_TO_MANY) { + $targetClass = $sqlWalker->getEntityManager()->getClassMetadata($assoc['targetEntity']); + $targetTableAlias = $sqlWalker->getSqlTableAlias($targetClass->table['name']); + $sourceTableAlias = $sqlWalker->getSqlTableAlias($class->table['name'], $dqlAlias); + + $sql .= $targetClass->getQuotedTableName($platform) . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']]; + + $first = true; + + foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) { + if ($first) $first = false; else $sql .= ' AND '; + + $sql .= $targetTableAlias . '.' . $sourceColumn + . ' = ' + . $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$targetColumn], $platform); + } + } else { // many-to-many + $targetClass = $sqlWalker->getEntityManager()->getClassMetadata($assoc['targetEntity']); + + $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']]; + $joinTable = $owningAssoc['joinTable']; + + // SQL table aliases + $joinTableAlias = $sqlWalker->getSqlTableAlias($joinTable['name']); + $sourceTableAlias = $sqlWalker->getSqlTableAlias($class->table['name'], $dqlAlias); + + // join to target table + $sql .= $targetClass->getQuotedJoinTableName($owningAssoc, $platform) . ' ' . $joinTableAlias . ' WHERE '; + + $joinColumns = $assoc['isOwningSide'] + ? $joinTable['joinColumns'] + : $joinTable['inverseJoinColumns']; + + $first = true; + + foreach ($joinColumns as $joinColumn) { + if ($first) $first = false; else $sql .= ' AND '; + + $sourceColumnName = $class->getQuotedColumnName( + $class->fieldNames[$joinColumn['referencedColumnName']], $platform + ); + + $sql .= $joinTableAlias . '.' . $joinColumn['name'] + . ' = ' + . $sourceTableAlias . '.' . $sourceColumnName; + } + } + + return '(' . $sql . ')'; + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $lexer = $parser->getLexer(); + + $parser->match(Lexer::T_SIZE); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->collectionPathExpression = $parser->CollectionValuedPathExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php b/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php new file mode 100644 index 0000000..02ffa26 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/SqrtFunction.php @@ -0,0 +1,61 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "SQRT" "(" SimpleArithmeticExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class SqrtFunction extends FunctionNode +{ + public $simpleArithmeticExpression; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + //TODO: Use platform to get SQL + return 'SQRT(' . $sqlWalker->walkSimpleArithmeticExpression($this->simpleArithmeticExpression) . ')'; + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->simpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php b/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php new file mode 100644 index 0000000..bfcbdef --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/SubstringFunction.php @@ -0,0 +1,82 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class SubstringFunction extends FunctionNode +{ + public $stringPrimary; + public $firstSimpleArithmeticExpression; + public $secondSimpleArithmeticExpression = null; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + $optionalSecondSimpleArithmeticExpression = null; + if ($this->secondSimpleArithmeticExpression !== null) { + $optionalSecondSimpleArithmeticExpression = $sqlWalker->walkSimpleArithmeticExpression($this->secondSimpleArithmeticExpression); + } + + return $sqlWalker->getConnection()->getDatabasePlatform()->getSubstringExpression( + $sqlWalker->walkStringPrimary($this->stringPrimary), + $sqlWalker->walkSimpleArithmeticExpression($this->firstSimpleArithmeticExpression), + $optionalSecondSimpleArithmeticExpression + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_COMMA); + + $this->firstSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + + $lexer = $parser->getLexer(); + if ($lexer->isNextToken(Lexer::T_COMMA)) { + $parser->match(Lexer::T_COMMA); + + $this->secondSimpleArithmeticExpression = $parser->SimpleArithmeticExpression(); + } + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/Functions/TrimFunction.php b/Doctrine/ORM/Query/AST/Functions/TrimFunction.php new file mode 100644 index 0000000..47b3cec --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/TrimFunction.php @@ -0,0 +1,100 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class TrimFunction extends FunctionNode +{ + public $leading; + public $trailing; + public $both; + public $trimChar = false; + public $stringPrimary; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + $pos = AbstractPlatform::TRIM_UNSPECIFIED; + if ($this->leading) { + $pos = AbstractPlatform::TRIM_LEADING; + } else if ($this->trailing) { + $pos = AbstractPlatform::TRIM_TRAILING; + } else if ($this->both) { + $pos = AbstractPlatform::TRIM_BOTH; + } + + return $sqlWalker->getConnection()->getDatabasePlatform()->getTrimExpression( + $sqlWalker->walkStringPrimary($this->stringPrimary), + $pos, + ($this->trimChar != false) ? $sqlWalker->getConnection()->quote($this->trimChar) : false + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $lexer = $parser->getLexer(); + + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + if (strcasecmp('leading', $lexer->lookahead['value']) === 0) { + $parser->match(Lexer::T_LEADING); + $this->leading = true; + } else if (strcasecmp('trailing', $lexer->lookahead['value']) === 0) { + $parser->match(Lexer::T_TRAILING); + $this->trailing = true; + } else if (strcasecmp('both', $lexer->lookahead['value']) === 0) { + $parser->match(Lexer::T_BOTH); + $this->both = true; + } + + if ($lexer->isNextToken(Lexer::T_STRING)) { + $parser->match(Lexer::T_STRING); + $this->trimChar = $lexer->token['value']; + } + + if ($this->leading || $this->trailing || $this->both || $this->trimChar) { + $parser->match(Lexer::T_FROM); + } + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } + +} diff --git a/Doctrine/ORM/Query/AST/Functions/UpperFunction.php b/Doctrine/ORM/Query/AST/Functions/UpperFunction.php new file mode 100644 index 0000000..acc8dd8 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Functions/UpperFunction.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Query\AST\Functions; + +use Doctrine\ORM\Query\Lexer; + +/** + * "UPPER" "(" StringPrimary ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class UpperFunction extends FunctionNode +{ + public $stringPrimary; + + /** + * @override + */ + public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + { + return $sqlWalker->getConnection()->getDatabasePlatform()->getUpperExpression( + $sqlWalker->walkSimpleArithmeticExpression($this->stringPrimary) + ); + } + + /** + * @override + */ + public function parse(\Doctrine\ORM\Query\Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->stringPrimary = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} + diff --git a/Doctrine/ORM/Query/AST/GroupByClause.php b/Doctrine/ORM/Query/AST/GroupByClause.php new file mode 100644 index 0000000..d5ae72f --- /dev/null +++ b/Doctrine/ORM/Query/AST/GroupByClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Description of GroupByClause + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class GroupByClause extends Node +{ + public $groupByItems = array(); + + public function __construct(array $groupByItems) + { + $this->groupByItems = $groupByItems; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkGroupByClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/HavingClause.php b/Doctrine/ORM/Query/AST/HavingClause.php new file mode 100644 index 0000000..08912b0 --- /dev/null +++ b/Doctrine/ORM/Query/AST/HavingClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Description of HavingClause + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class HavingClause extends Node +{ + public $conditionalExpression; + + public function __construct($conditionalExpression) + { + $this->conditionalExpression = $conditionalExpression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkHavingClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php b/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php new file mode 100644 index 0000000..bf168c3 --- /dev/null +++ b/Doctrine/ORM/Query/AST/IdentificationVariableDeclaration.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class IdentificationVariableDeclaration extends Node +{ + public $rangeVariableDeclaration = null; + public $indexBy = null; + public $joinVariableDeclarations = array(); + + public function __construct($rangeVariableDecl, $indexBy, array $joinVariableDecls) + { + $this->rangeVariableDeclaration = $rangeVariableDecl; + $this->indexBy = $indexBy; + $this->joinVariableDeclarations = $joinVariableDecls; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkIdentificationVariableDeclaration($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/InExpression.php b/Doctrine/ORM/Query/AST/InExpression.php new file mode 100644 index 0000000..b1da401 --- /dev/null +++ b/Doctrine/ORM/Query/AST/InExpression.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * InExpression ::= StateFieldPathExpression ["NOT"] "IN" "(" (Literal {"," Literal}* | Subselect) ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class InExpression extends Node +{ + public $not; + public $pathExpression; + public $literals = array(); + public $subselect; + + public function __construct($pathExpression) + { + $this->pathExpression = $pathExpression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkInExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/IndexBy.php b/Doctrine/ORM/Query/AST/IndexBy.php new file mode 100644 index 0000000..b657be7 --- /dev/null +++ b/Doctrine/ORM/Query/AST/IndexBy.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * IndexBy ::= "INDEX" "BY" SimpleStateFieldPathExpression + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class IndexBy extends Node +{ + public $simpleStateFieldPathExpression = null; + + public function __construct($simpleStateFieldPathExpression) + { + $this->simpleStateFieldPathExpression = $simpleStateFieldPathExpression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkIndexBy($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/InputParameter.php b/Doctrine/ORM/Query/AST/InputParameter.php new file mode 100644 index 0000000..cf04d70 --- /dev/null +++ b/Doctrine/ORM/Query/AST/InputParameter.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Description of InputParameter + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class InputParameter extends Node +{ + public $isNamed; + public $name; + + /** + * @param string $value + */ + public function __construct($value) + { + if (strlen($value) == 1) { + throw \Doctrine\ORM\Query\QueryException::invalidParameterFormat($value); + } + + $param = substr($value, 1); + $this->isNamed = ! is_numeric($param); + $this->name = $param; + } + + public function dispatch($walker) + { + return $walker->walkInputParameter($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/InstanceOfExpression.php b/Doctrine/ORM/Query/AST/InstanceOfExpression.php new file mode 100644 index 0000000..3aefd61 --- /dev/null +++ b/Doctrine/ORM/Query/AST/InstanceOfExpression.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (AbstractSchemaName | InputParameter) + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class InstanceOfExpression extends Node +{ + public $not; + public $identificationVariable; + public $value; + + public function __construct($identVariable) + { + $this->identificationVariable = $identVariable; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkInstanceOfExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/Join.php b/Doctrine/ORM/Query/AST/Join.php new file mode 100644 index 0000000..310a418 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Join.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression + * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Join extends Node +{ + const JOIN_TYPE_LEFT = 1; + const JOIN_TYPE_LEFTOUTER = 2; + const JOIN_TYPE_INNER = 3; + + public $joinType = self::JOIN_TYPE_INNER; + public $joinAssociationPathExpression = null; + public $aliasIdentificationVariable = null; + public $conditionalExpression = null; + + public function __construct($joinType, $joinAssocPathExpr, $aliasIdentVar) + { + $this->joinType = $joinType; + $this->joinAssociationPathExpression = $joinAssocPathExpr; + $this->aliasIdentificationVariable = $aliasIdentVar; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkJoin($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php b/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php new file mode 100644 index 0000000..7f87e52 --- /dev/null +++ b/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField) + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class JoinAssociationPathExpression extends Node +{ + public $identificationVariable; + public $associationField; + + public function __construct($identificationVariable, $associationField) + { + $this->identificationVariable = $identificationVariable; + $this->associationField = $associationField; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkJoinPathExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php b/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php new file mode 100644 index 0000000..687eba3 --- /dev/null +++ b/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * JoinVariableDeclaration ::= Join [IndexBy] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class JoinVariableDeclaration extends Node +{ + public $join = null; + public $indexBy = null; + + public function __construct($join, $indexBy) + { + $this->join = $join; + $this->indexBy = $indexBy; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkJoinVariableDeclaration($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/LikeExpression.php b/Doctrine/ORM/Query/AST/LikeExpression.php new file mode 100644 index 0000000..7985be4 --- /dev/null +++ b/Doctrine/ORM/Query/AST/LikeExpression.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * LikeExpression ::= StringExpression ["NOT"] "LIKE" string ["ESCAPE" char] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class LikeExpression extends Node +{ + public $not; + public $stringExpression; + public $stringPattern; + public $escapeChar; + + public function __construct($stringExpression, $stringPattern, $escapeChar = null) + { + $this->stringExpression = $stringExpression; + $this->stringPattern = $stringPattern; + $this->escapeChar = $escapeChar; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkLikeExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Literal.php b/Doctrine/ORM/Query/AST/Literal.php new file mode 100644 index 0000000..d3acb96 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Literal.php @@ -0,0 +1,24 @@ +type = $type; + $this->value = $value; + } + + public function dispatch($walker) + { + return $walker->walkLiteral($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Node.php b/Doctrine/ORM/Query/AST/Node.php new file mode 100644 index 0000000..adaf06c --- /dev/null +++ b/Doctrine/ORM/Query/AST/Node.php @@ -0,0 +1,98 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Abstract class of an AST node + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +abstract class Node +{ + /** + * Double-dispatch method, supposed to dispatch back to the walker. + * + * Implementation is not mandatory for all nodes. + * + * @param $walker + */ + public function dispatch($walker) + { + throw ASTException::noDispatchForNode($this); + } + + /** + * Dumps the AST Node into a string representation for information purpose only + * + * @return string + */ + public function __toString() + { + return $this->dump($this); + } + + public function dump($obj) + { + static $ident = 0; + + $str = ''; + + if ($obj instanceof Node) { + $str .= get_class($obj) . '(' . PHP_EOL; + $props = get_object_vars($obj); + + foreach ($props as $name => $prop) { + $ident += 4; + $str .= str_repeat(' ', $ident) . '"' . $name . '": ' + . $this->dump($prop) . ',' . PHP_EOL; + $ident -= 4; + } + + $str .= str_repeat(' ', $ident) . ')'; + } else if (is_array($obj)) { + $ident += 4; + $str .= 'array('; + $some = false; + + foreach ($obj as $k => $v) { + $str .= PHP_EOL . str_repeat(' ', $ident) . '"' + . $k . '" => ' . $this->dump($v) . ','; + $some = true; + } + + $ident -= 4; + $str .= ($some ? PHP_EOL . str_repeat(' ', $ident) : '') . ')'; + } else if (is_object($obj)) { + $str .= 'instanceof(' . get_class($obj) . ')'; + } else { + $str .= var_export($obj, true); + } + + return $str; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/NullComparisonExpression.php b/Doctrine/ORM/Query/AST/NullComparisonExpression.php new file mode 100644 index 0000000..aa205b7 --- /dev/null +++ b/Doctrine/ORM/Query/AST/NullComparisonExpression.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class NullComparisonExpression extends Node +{ + public $not; + public $expression; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkNullComparisonExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/OrderByClause.php b/Doctrine/ORM/Query/AST/OrderByClause.php new file mode 100644 index 0000000..7e23059 --- /dev/null +++ b/Doctrine/ORM/Query/AST/OrderByClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class OrderByClause extends Node +{ + public $orderByItems = array(); + + public function __construct(array $orderByItems) + { + $this->orderByItems = $orderByItems; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkOrderByClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/OrderByItem.php b/Doctrine/ORM/Query/AST/OrderByItem.php new file mode 100644 index 0000000..a05bac3 --- /dev/null +++ b/Doctrine/ORM/Query/AST/OrderByItem.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * OrderByItem ::= (ResultVariable | StateFieldPathExpression) ["ASC" | "DESC"] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class OrderByItem extends Node +{ + public $expression; + public $type; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function isAsc() + { + return strtoupper($this->type) == 'ASC'; + } + + public function isDesc() + { + return strtoupper($this->type) == 'DESC'; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkOrderByItem($this); + } +} diff --git a/Doctrine/ORM/Query/AST/PartialObjectExpression.php b/Doctrine/ORM/Query/AST/PartialObjectExpression.php new file mode 100644 index 0000000..08fd564 --- /dev/null +++ b/Doctrine/ORM/Query/AST/PartialObjectExpression.php @@ -0,0 +1,15 @@ +identificationVariable = $identificationVariable; + $this->partialFieldSet = $partialFieldSet; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/PathExpression.php b/Doctrine/ORM/Query/AST/PathExpression.php new file mode 100644 index 0000000..45042c2 --- /dev/null +++ b/Doctrine/ORM/Query/AST/PathExpression.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression + * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression + * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField + * StateField ::= {EmbeddedClassStateField "."}* SimpleStateField + * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class PathExpression extends Node +{ + const TYPE_COLLECTION_VALUED_ASSOCIATION = 2; + const TYPE_SINGLE_VALUED_ASSOCIATION = 4; + const TYPE_STATE_FIELD = 8; + + public $type; + public $expectedType; + public $identificationVariable; + public $field; + + public function __construct($expectedType, $identificationVariable, $field = null) + { + $this->expectedType = $expectedType; + $this->identificationVariable = $identificationVariable; + $this->field = $field; + } + + public function dispatch($walker) + { + return $walker->walkPathExpression($this); + } +} diff --git a/Doctrine/ORM/Query/AST/QuantifiedExpression.php b/Doctrine/ORM/Query/AST/QuantifiedExpression.php new file mode 100644 index 0000000..11b60ed --- /dev/null +++ b/Doctrine/ORM/Query/AST/QuantifiedExpression.php @@ -0,0 +1,68 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class QuantifiedExpression extends Node +{ + public $type; + public $subselect; + + public function __construct($subselect) + { + $this->subselect = $subselect; + } + + public function isAll() + { + return strtoupper($this->type) == 'ALL'; + } + + public function isAny() + { + return strtoupper($this->type) == 'ANY'; + } + + public function isSome() + { + return strtoupper($this->type) == 'SOME'; + } + + /** + * @override + */ + public function dispatch($sqlWalker) + { + return $sqlWalker->walkQuantifiedExpression($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php b/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php new file mode 100644 index 0000000..7a01bdc --- /dev/null +++ b/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class RangeVariableDeclaration extends Node +{ + public $abstractSchemaName; + public $aliasIdentificationVariable; + + public function __construct($abstractSchemaName, $aliasIdentificationVar) + { + $this->abstractSchemaName = $abstractSchemaName; + $this->aliasIdentificationVariable = $aliasIdentificationVar; + } + + public function dispatch($walker) + { + return $walker->walkRangeVariableDeclaration($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SelectClause.php b/Doctrine/ORM/Query/AST/SelectClause.php new file mode 100644 index 0000000..ed9fbaa --- /dev/null +++ b/Doctrine/ORM/Query/AST/SelectClause.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SelectClause = "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SelectClause extends Node +{ + public $isDistinct; + public $selectExpressions = array(); + + public function __construct(array $selectExpressions, $isDistinct) + { + $this->isDistinct = $isDistinct; + $this->selectExpressions = $selectExpressions; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSelectClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SelectExpression.php b/Doctrine/ORM/Query/AST/SelectExpression.php new file mode 100644 index 0000000..f1793f0 --- /dev/null +++ b/Doctrine/ORM/Query/AST/SelectExpression.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SelectExpression ::= IdentificationVariable ["." "*"] | StateFieldPathExpression | + * (AggregateExpression | "(" Subselect ")") [["AS"] FieldAliasIdentificationVariable] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SelectExpression extends Node +{ + public $expression; + public $fieldIdentificationVariable; + + public function __construct($expression, $fieldIdentificationVariable) + { + $this->expression = $expression; + $this->fieldIdentificationVariable = $fieldIdentificationVariable; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSelectExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SelectStatement.php b/Doctrine/ORM/Query/AST/SelectStatement.php new file mode 100644 index 0000000..01eed37 --- /dev/null +++ b/Doctrine/ORM/Query/AST/SelectStatement.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SelectStatement = SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SelectStatement extends Node +{ + public $selectClause; + public $fromClause; + public $whereClause; + public $groupByClause; + public $havingClause; + public $orderByClause; + + public function __construct($selectClause, $fromClause) { + $this->selectClause = $selectClause; + $this->fromClause = $fromClause; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSelectStatement($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SimpleArithmeticExpression.php b/Doctrine/ORM/Query/AST/SimpleArithmeticExpression.php new file mode 100644 index 0000000..3fb6f05 --- /dev/null +++ b/Doctrine/ORM/Query/AST/SimpleArithmeticExpression.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SimpleArithmeticExpression extends Node +{ + public $arithmeticTerms = array(); + + public function __construct(array $arithmeticTerms) + { + $this->arithmeticTerms = $arithmeticTerms; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSimpleArithmeticExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SimpleSelectClause.php b/Doctrine/ORM/Query/AST/SimpleSelectClause.php new file mode 100644 index 0000000..e3b93e7 --- /dev/null +++ b/Doctrine/ORM/Query/AST/SimpleSelectClause.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SimpleSelectClause extends Node +{ + public $isDistinct = false; + public $simpleSelectExpression; + + public function __construct($simpleSelectExpression, $isDistinct) + { + $this->simpleSelectExpression = $simpleSelectExpression; + $this->isDistinct = $isDistinct; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSimpleSelectClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SimpleSelectExpression.php b/Doctrine/ORM/Query/AST/SimpleSelectExpression.php new file mode 100644 index 0000000..e25d38b --- /dev/null +++ b/Doctrine/ORM/Query/AST/SimpleSelectExpression.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SimpleSelectExpression ::= StateFieldPathExpression | IdentificationVariable + * | (AggregateExpression [["AS"] FieldAliasIdentificationVariable]) + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SimpleSelectExpression extends Node +{ + public $expression; + public $fieldIdentificationVariable; + + public function __construct($expression) + { + $this->expression = $expression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSimpleSelectExpression($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/Subselect.php b/Doctrine/ORM/Query/AST/Subselect.php new file mode 100644 index 0000000..fa5d335 --- /dev/null +++ b/Doctrine/ORM/Query/AST/Subselect.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Subselect extends Node +{ + public $simpleSelectClause; + public $subselectFromClause; + public $whereClause; + public $groupByClause; + public $havingClause; + public $orderByClause; + + public function __construct($simpleSelectClause, $subselectFromClause) + { + $this->simpleSelectClause = $simpleSelectClause; + $this->subselectFromClause = $subselectFromClause; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSubselect($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/SubselectFromClause.php b/Doctrine/ORM/Query/AST/SubselectFromClause.php new file mode 100644 index 0000000..44d2b59 --- /dev/null +++ b/Doctrine/ORM/Query/AST/SubselectFromClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SubselectFromClause extends Node +{ + public $identificationVariableDeclarations = array(); + + public function __construct(array $identificationVariableDeclarations) + { + $this->identificationVariableDeclarations = $identificationVariableDeclarations; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkSubselectFromClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/UpdateClause.php b/Doctrine/ORM/Query/AST/UpdateClause.php new file mode 100644 index 0000000..158cf24 --- /dev/null +++ b/Doctrine/ORM/Query/AST/UpdateClause.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * UpdateClause ::= "UPDATE" AbstractSchemaName [["AS"] AliasIdentificationVariable] "SET" UpdateItem {"," UpdateItem}* + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class UpdateClause extends Node +{ + public $abstractSchemaName; + public $aliasIdentificationVariable; + public $updateItems = array(); + + public function __construct($abstractSchemaName, array $updateItems) + { + $this->abstractSchemaName = $abstractSchemaName; + $this->updateItems = $updateItems; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkUpdateClause($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/UpdateItem.php b/Doctrine/ORM/Query/AST/UpdateItem.php new file mode 100644 index 0000000..5f8b678 --- /dev/null +++ b/Doctrine/ORM/Query/AST/UpdateItem.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * UpdateItem ::= [IdentificationVariable "."] {StateField | SingleValuedAssociationField} "=" NewValue + * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | + * EnumPrimary | SimpleEntityExpression | "NULL" + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class UpdateItem extends Node +{ + public $pathExpression; + public $newValue; + + public function __construct($pathExpression, $newValue) + { + $this->pathExpression = $pathExpression; + $this->newValue = $newValue; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkUpdateItem($this); + } +} + diff --git a/Doctrine/ORM/Query/AST/UpdateStatement.php b/Doctrine/ORM/Query/AST/UpdateStatement.php new file mode 100644 index 0000000..7bf40bb --- /dev/null +++ b/Doctrine/ORM/Query/AST/UpdateStatement.php @@ -0,0 +1,49 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * UpdateStatement = UpdateClause [WhereClause] + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class UpdateStatement extends Node +{ + public $updateClause; + public $whereClause; + + public function __construct($updateClause) + { + $this->updateClause = $updateClause; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkUpdateStatement($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/AST/WhereClause.php b/Doctrine/ORM/Query/AST/WhereClause.php new file mode 100644 index 0000000..5d3f9c9 --- /dev/null +++ b/Doctrine/ORM/Query/AST/WhereClause.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * WhereClause ::= "WHERE" ConditionalExpression + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class WhereClause extends Node +{ + public $conditionalExpression; + + public function __construct($conditionalExpression) + { + $this->conditionalExpression = $conditionalExpression; + } + + public function dispatch($sqlWalker) + { + return $sqlWalker->walkWhereClause($this); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php b/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php new file mode 100644 index 0000000..7879b0f --- /dev/null +++ b/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\ORM\Query\Exec; + +use Doctrine\DBAL\Connection; + +/** + * Base class for SQL statement executors. + * + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.doctrine-project.org + * @since 2.0 + * @todo Rename: AbstractSQLExecutor + */ +abstract class AbstractSqlExecutor +{ + protected $_sqlStatements; + + /** + * Gets the SQL statements that are executed by the executor. + * + * @return array All the SQL update statements. + */ + public function getSqlStatements() + { + return $this->_sqlStatements; + } + + /** + * Executes all sql statements. + * + * @param Doctrine\DBAL\Connection $conn The database connection that is used to execute the queries. + * @param array $params The parameters. + * @return Doctrine\DBAL\Driver\Statement + */ + abstract public function execute(Connection $conn, array $params, array $types); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php b/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php new file mode 100644 index 0000000..8ddb78f --- /dev/null +++ b/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php @@ -0,0 +1,129 @@ +. + */ + +namespace Doctrine\ORM\Query\Exec; + +use Doctrine\DBAL\Connection, + Doctrine\ORM\Query\AST; + +/** + * Executes the SQL statements for bulk DQL DELETE statements on classes in + * Class Table Inheritance (JOINED). + * + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + */ +class MultiTableDeleteExecutor extends AbstractSqlExecutor +{ + private $_createTempTableSql; + private $_dropTempTableSql; + private $_insertSql; + + /** + * Initializes a new MultiTableDeleteExecutor. + * + * @param Node $AST The root AST node of the DQL query. + * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. + * @internal Any SQL construction and preparation takes place in the constructor for + * best performance. With a query cache the executor will be cached. + */ + public function __construct(AST\Node $AST, $sqlWalker) + { + $em = $sqlWalker->getEntityManager(); + $conn = $em->getConnection(); + $platform = $conn->getDatabasePlatform(); + + $primaryClass = $em->getClassMetadata($AST->deleteClause->abstractSchemaName); + $primaryDqlAlias = $AST->deleteClause->aliasIdentificationVariable; + $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); + + $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); + $idColumnNames = $rootClass->getIdentifierColumnNames(); + $idColumnList = implode(', ', $idColumnNames); + + // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() + $this->_insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' + . ' SELECT t0.' . implode(', t0.', $idColumnNames); + $sqlWalker->setSqlTableAlias($primaryClass->table['name'] . $primaryDqlAlias, 't0'); + $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $primaryDqlAlias); + $fromClause = new AST\FromClause(array(new AST\IdentificationVariableDeclaration($rangeDecl, null, array()))); + $this->_insertSql .= $sqlWalker->walkFromClause($fromClause); + + // Append WHERE clause, if there is one. + if ($AST->whereClause) { + $this->_insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); + } + + // 2. Create ID subselect statement used in DELETE ... WHERE ... IN (subselect) + $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; + + // 3. Create and store DELETE statements + $classNames = array_merge($primaryClass->parentClasses, array($primaryClass->name), $primaryClass->subClasses); + foreach (array_reverse($classNames) as $className) { + $tableName = $em->getClassMetadata($className)->getQuotedTableName($platform); + $this->_sqlStatements[] = 'DELETE FROM ' . $tableName + . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; + } + + // 4. Store DDL for temporary identifier table. + $columnDefinitions = array(); + foreach ($idColumnNames as $idColumnName) { + $columnDefinitions[$idColumnName] = array( + 'notnull' => true, + 'type' => \Doctrine\DBAL\Types\Type::getType($rootClass->getTypeOfColumn($idColumnName)) + ); + } + $this->_createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; + $this->_dropTempTableSql = 'DROP TABLE ' . $tempTable; + } + + /** + * Executes all SQL statements. + * + * @param Doctrine\DBAL\Connection $conn The database connection that is used to execute the queries. + * @param array $params The parameters. + * @override + */ + public function execute(Connection $conn, array $params, array $types) + { + $numDeleted = 0; + + // Create temporary id table + $conn->executeUpdate($this->_createTempTableSql); + + // Insert identifiers + $numDeleted = $conn->executeUpdate($this->_insertSql, $params, $types); + + // Execute DELETE statements + foreach ($this->_sqlStatements as $sql) { + $conn->executeUpdate($sql); + } + + // Drop temporary table + $conn->executeUpdate($this->_dropTempTableSql); + + return $numDeleted; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php b/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php new file mode 100644 index 0000000..7298db9 --- /dev/null +++ b/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php @@ -0,0 +1,161 @@ +. + */ + +namespace Doctrine\ORM\Query\Exec; + +use Doctrine\DBAL\Connection, + Doctrine\DBAL\Types\Type, + Doctrine\ORM\Query\AST; + +/** + * Executes the SQL statements for bulk DQL UPDATE statements on classes in + * Class Table Inheritance (JOINED). + * + * @author Roman Borschel + * @since 2.0 + */ +class MultiTableUpdateExecutor extends AbstractSqlExecutor +{ + private $_createTempTableSql; + private $_dropTempTableSql; + private $_insertSql; + private $_sqlParameters = array(); + private $_numParametersInUpdateClause = 0; + + /** + * Initializes a new MultiTableUpdateExecutor. + * + * @param Node $AST The root AST node of the DQL query. + * @param SqlWalker $sqlWalker The walker used for SQL generation from the AST. + * @internal Any SQL construction and preparation takes place in the constructor for + * best performance. With a query cache the executor will be cached. + */ + public function __construct(AST\Node $AST, $sqlWalker) + { + $em = $sqlWalker->getEntityManager(); + $conn = $em->getConnection(); + $platform = $conn->getDatabasePlatform(); + + $updateClause = $AST->updateClause; + + $primaryClass = $sqlWalker->getEntityManager()->getClassMetadata($updateClause->abstractSchemaName); + $rootClass = $em->getClassMetadata($primaryClass->rootEntityName); + + $updateItems = $updateClause->updateItems; + + $tempTable = $platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); + $idColumnNames = $rootClass->getIdentifierColumnNames(); + $idColumnList = implode(', ', $idColumnNames); + + // 1. Create an INSERT INTO temptable ... SELECT identifiers WHERE $AST->getWhereClause() + $this->_insertSql = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ')' + . ' SELECT t0.' . implode(', t0.', $idColumnNames); + $sqlWalker->setSqlTableAlias($primaryClass->table['name'] . $updateClause->aliasIdentificationVariable, 't0'); + $rangeDecl = new AST\RangeVariableDeclaration($primaryClass->name, $updateClause->aliasIdentificationVariable); + $fromClause = new AST\FromClause(array(new AST\IdentificationVariableDeclaration($rangeDecl, null, array()))); + $this->_insertSql .= $sqlWalker->walkFromClause($fromClause); + + // 2. Create ID subselect statement used in UPDATE ... WHERE ... IN (subselect) + $idSubselect = 'SELECT ' . $idColumnList . ' FROM ' . $tempTable; + + // 3. Create and store UPDATE statements + $classNames = array_merge($primaryClass->parentClasses, array($primaryClass->name), $primaryClass->subClasses); + $i = -1; + + foreach (array_reverse($classNames) as $className) { + $affected = false; + $class = $em->getClassMetadata($className); + $updateSql = 'UPDATE ' . $class->getQuotedTableName($platform) . ' SET '; + + foreach ($updateItems as $updateItem) { + $field = $updateItem->pathExpression->field; + if (isset($class->fieldMappings[$field]) && ! isset($class->fieldMappings[$field]['inherited']) || + isset($class->associationMappings[$field]) && ! isset($class->associationMappings[$field]['inherited'])) { + $newValue = $updateItem->newValue; + + if ( ! $affected) { + $affected = true; + ++$i; + } else { + $updateSql .= ', '; + } + + $updateSql .= $sqlWalker->walkUpdateItem($updateItem); + + //FIXME: parameters can be more deeply nested. traverse the tree. + //FIXME (URGENT): With query cache the parameter is out of date. Move to execute() stage. + if ($newValue instanceof AST\InputParameter) { + $paramKey = $newValue->name; + $this->_sqlParameters[$i][] = $sqlWalker->getQuery()->getParameter($paramKey); + ++$this->_numParametersInUpdateClause; + } + } + } + + if ($affected) { + $this->_sqlStatements[$i] = $updateSql . ' WHERE (' . $idColumnList . ') IN (' . $idSubselect . ')'; + } + } + + // Append WHERE clause to insertSql, if there is one. + if ($AST->whereClause) { + $this->_insertSql .= $sqlWalker->walkWhereClause($AST->whereClause); + } + + // 4. Store DDL for temporary identifier table. + $columnDefinitions = array(); + foreach ($idColumnNames as $idColumnName) { + $columnDefinitions[$idColumnName] = array( + 'notnull' => true, + 'type' => Type::getType($rootClass->getTypeOfColumn($idColumnName)) + ); + } + $this->_createTempTableSql = $platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable . ' (' + . $platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; + $this->_dropTempTableSql = 'DROP TABLE ' . $tempTable; + } + + /** + * Executes all SQL statements. + * + * @param Connection $conn The database connection that is used to execute the queries. + * @param array $params The parameters. + * @override + */ + public function execute(Connection $conn, array $params, array $types) + { + $numUpdated = 0; + + // Create temporary id table + $conn->executeUpdate($this->_createTempTableSql); + + // Insert identifiers. Parameters from the update clause are cut off. + $numUpdated = $conn->executeUpdate($this->_insertSql, array_slice($params, $this->_numParametersInUpdateClause), $types); + + // Execute UPDATE statements + for ($i=0, $count=count($this->_sqlStatements); $i<$count; ++$i) { + $conn->executeUpdate($this->_sqlStatements[$i], isset($this->_sqlParameters[$i]) ? $this->_sqlParameters[$i] : array()); + } + + // Drop temporary table + $conn->executeUpdate($this->_dropTempTableSql); + + return $numUpdated; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php b/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php new file mode 100644 index 0000000..4e08e0f --- /dev/null +++ b/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php @@ -0,0 +1,48 @@ +. + */ + +namespace Doctrine\ORM\Query\Exec; + +use Doctrine\DBAL\Connection, + Doctrine\ORM\Query\AST\SelectStatement, + Doctrine\ORM\Query\SqlWalker; + +/** + * Executor that executes the SQL statement for simple DQL SELECT statements. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Roman Borschel + * @version $Revision$ + * @link www.doctrine-project.org + * @since 2.0 + */ +class SingleSelectExecutor extends AbstractSqlExecutor +{ + public function __construct(SelectStatement $AST, SqlWalker $sqlWalker) + { + $this->_sqlStatements = $sqlWalker->walkSelectStatement($AST); + } + + public function execute(Connection $conn, array $params, array $types) + { + return $conn->executeQuery($this->_sqlStatements, $params, $types); + } +} diff --git a/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php b/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php new file mode 100644 index 0000000..94db13b --- /dev/null +++ b/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Query\Exec; + +use Doctrine\DBAL\Connection, + Doctrine\ORM\Query\AST; + +/** + * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes + * that are mapped to a single table. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @author Roman Borschel + * @version $Revision$ + * @link www.doctrine-project.org + * @since 2.0 + * @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor. + */ +class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor +{ + public function __construct(AST\Node $AST, $sqlWalker) + { + if ($AST instanceof AST\UpdateStatement) { + $this->_sqlStatements = $sqlWalker->walkUpdateStatement($AST); + } else if ($AST instanceof AST\DeleteStatement) { + $this->_sqlStatements = $sqlWalker->walkDeleteStatement($AST); + } + } + + public function execute(Connection $conn, array $params, array $types) + { + return $conn->executeUpdate($this->_sqlStatements, $params, $types); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr.php b/Doctrine/ORM/Query/Expr.php new file mode 100644 index 0000000..a420b67 --- /dev/null +++ b/Doctrine/ORM/Query/Expr.php @@ -0,0 +1,569 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * This class is used to generate DQL expressions via a set of PHP static functions + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + * @todo Rename: ExpressionBuilder + */ +class Expr +{ + /** + * Creates a conjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?1) AND (u.role = ?2) + * $expr->andX('u.type = ?1', 'u.role = ?2')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * @return Expr\Andx + */ + public function andX($x = null) + { + return new Expr\Andx(func_get_args()); + } + + /** + * Creates a disjunction of the given boolean expressions. + * + * Example: + * + * [php] + * // (u.type = ?1) OR (u.role = ?2) + * $q->where($q->expr()->orX('u.type = ?1', 'u.role = ?2')); + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * @return Expr\Orx + */ + public function orX($x = null) + { + return new Expr\Orx(func_get_args()); + } + + /** + * Creates an ASCending order expression. + * + * @param $sort + * @return OrderBy + */ + public function asc($expr) + { + return new Expr\OrderBy($expr, 'ASC'); + } + + /** + * Creates a DESCending order expression. + * + * @param $sort + * @return OrderBy + */ + public function desc($expr) + { + return new Expr\OrderBy($expr, 'DESC'); + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ?1 + * $expr->eq('u.id', '?1'); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function eq($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::EQ, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <> . Example: + * + * [php] + * // u.id <> ?1 + * $q->where($q->expr()->neq('u.id', '?1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function neq($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::NEQ, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a < . Example: + * + * [php] + * // u.id < ?1 + * $q->where($q->expr()->lt('u.id', '?1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function lt($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::LT, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <= . Example: + * + * [php] + * // u.id <= ?1 + * $q->where($q->expr()->lte('u.id', '?1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function lte($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::LTE, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a > . Example: + * + * [php] + * // u.id > ?1 + * $q->where($q->expr()->gt('u.id', '?1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function gt($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::GT, $y); + } + + /** + * Creates an instance of Expr\Comparison, with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a >= . Example: + * + * [php] + * // u.id >= ?1 + * $q->where($q->expr()->gte('u.id', '?1')); + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Comparison + */ + public function gte($x, $y) + { + return new Expr\Comparison($x, Expr\Comparison::GTE, $y); + } + + /** + * Creates an instance of AVG() function, with the given argument. + * + * @param mixed $x Argument to be used in AVG() function. + * @return Expr\Func + */ + public function avg($x) + { + return new Expr\Func('AVG', array($x)); + } + + /** + * Creates an instance of MAX() function, with the given argument. + * + * @param mixed $x Argument to be used in MAX() function. + * @return Expr\Func + */ + public function max($x) + { + return new Expr\Func('MAX', array($x)); + } + + /** + * Creates an instance of MIN() function, with the given argument. + * + * @param mixed $x Argument to be used in MIN() function. + * @return Expr\Func + */ + public function min($x) + { + return new Expr\Func('MIN', array($x)); + } + + /** + * Creates an instance of COUNT() function, with the given argument. + * + * @param mixed $x Argument to be used in COUNT() function. + * @return Expr\Func + */ + public function count($x) + { + return new Expr\Func('COUNT', array($x)); + } + + /** + * Creates an instance of COUNT(DISTINCT) function, with the given argument. + * + * @param mixed $x Argument to be used in COUNT(DISTINCT) function. + * @return string + */ + public function countDistinct($x) + { + return 'COUNT(DISTINCT ' . implode(', ', func_get_args()) . ')'; + } + + /** + * Creates an instance of EXISTS() function, with the given DQL Subquery. + * + * @param mixed $subquery DQL Subquery to be used in EXISTS() function. + * @return Expr\Func + */ + public function exists($subquery) + { + return new Expr\Func('EXISTS', array($subquery)); + } + + /** + * Creates an instance of ALL() function, with the given DQL Subquery. + * + * @param mixed $subquery DQL Subquery to be used in ALL() function. + * @return Expr\Func + */ + public function all($subquery) + { + return new Expr\Func('ALL', array($subquery)); + } + + /** + * Creates a SOME() function expression with the given DQL subquery. + * + * @param mixed $subquery DQL Subquery to be used in SOME() function. + * @return Expr\Func + */ + public function some($subquery) + { + return new Expr\Func('SOME', array($subquery)); + } + + /** + * Creates an ANY() function expression with the given DQL subquery. + * + * @param mixed $subquery DQL Subquery to be used in ANY() function. + * @return Expr\Func + */ + public function any($subquery) + { + return new Expr\Func('ANY', array($subquery)); + } + + /** + * Creates a negation expression of the given restriction. + * + * @param mixed $restriction Restriction to be used in NOT() function. + * @return Expr\Func + */ + public function not($restriction) + { + return new Expr\Func('NOT', array($restriction)); + } + + /** + * Creates an ABS() function expression with the given argument. + * + * @param mixed $x Argument to be used in ABS() function. + * @return Expr\Func + */ + public function abs($x) + { + return new Expr\Func('ABS', array($x)); + } + + /** + * Creates a product mathematical expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a * . Example: + * + * [php] + * // u.salary * u.percentAnualSalaryIncrease + * $q->expr()->prod('u.salary', 'u.percentAnualSalaryIncrease') + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Math + */ + public function prod($x, $y) + { + return new Expr\Math($x, '*', $y); + } + + /** + * Creates a difference mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a - . Example: + * + * [php] + * // u.monthlySubscriptionCount - 1 + * $q->expr()->diff('u.monthlySubscriptionCount', '1') + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Math + */ + public function diff($x, $y) + { + return new Expr\Math($x, '-', $y); + } + + /** + * Creates a sum mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a + . Example: + * + * [php] + * // u.numChildren + 1 + * $q->expr()->diff('u.numChildren', '1') + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Math + */ + public function sum($x, $y) + { + return new Expr\Math($x, '+', $y); + } + + /** + * Creates a quotient mathematical expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a / . Example: + * + * [php] + * // u.total / u.period + * $expr->quot('u.total', 'u.period') + * + * @param mixed $x Left expression + * @param mixed $y Right expression + * @return Expr\Math + */ + public function quot($x, $y) + { + return new Expr\Math($x, '/', $y); + } + + /** + * Creates a SQRT() function expression with the given argument. + * + * @param mixed $x Argument to be used in SQRT() function. + * @return Expr\Func + */ + public function sqrt($x) + { + return new Expr\Func('SQRT', array($x)); + } + + /** + * Creates an IN() expression with the given arguments. + * + * @param string $x Field in string format to be restricted by IN() function + * @param mixed $y Argument to be used in IN() function. + * @return Expr\Func + */ + public function in($x, $y) + { + if (is_array($y)) { + foreach ($y as &$literal) { + if ( ! ($literal instanceof Expr\Literal)) { + $literal = $this->_quoteLiteral($literal); + } + } + } + return new Expr\Func($x . ' IN', (array) $y); + } + + /** + * Creates a NOT IN() expression with the given arguments. + * + * @param string $x Field in string format to be restricted by NOT IN() function + * @param mixed $y Argument to be used in NOT IN() function. + * @return Expr\Func + */ + public function notIn($x, $y) + { + if (is_array($y)) { + foreach ($y as &$literal) { + if ( ! ($literal instanceof Expr\Literal)) { + $literal = $this->_quoteLiteral($literal); + } + } + } + return new Expr\Func($x . ' NOT IN', (array) $y); + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x Field in string format to be inspected by LIKE() comparison. + * @param mixed $y Argument to be used in LIKE() comparison. + * @return Expr\Comparison + */ + public function like($x, $y) + { + return new Expr\Comparison($x, 'LIKE', $y); + } + + /** + * Creates a CONCAT() function expression with the given arguments. + * + * @param mixed $x First argument to be used in CONCAT() function. + * @param mixed $x Second argument to be used in CONCAT() function. + * @return Expr\Func + */ + public function concat($x, $y) + { + return new Expr\Func('CONCAT', array($x, $y)); + } + + /** + * Creates a SUBSTRING() function expression with the given arguments. + * + * @param mixed $x Argument to be used as string to be cropped by SUBSTRING() function. + * @param integer $from Initial offset to start cropping string. May accept negative values. + * @param integer $len Length of crop. May accept negative values. + * @return Expr\Func + */ + public function substring($x, $from, $len = null) + { + $args = array($x, $from); + if (null !== $len) { + $args[] = $len; + } + return new Expr\Func('SUBSTRING', $args); + } + + /** + * Creates a LOWER() function expression with the given argument. + * + * @param mixed $x Argument to be used in LOWER() function. + * @return Expr\Func A LOWER function expression. + */ + public function lower($x) + { + return new Expr\Func('LOWER', array($x)); + } + + /** + * Creates an UPPER() function expression with the given argument. + * + * @param mixed $x Argument to be used in UPPER() function. + * @return Expr\Func An UPPER function expression. + */ + public function upper($x) + { + return new Expr\Func('UPPER', array($x)); + } + + /** + * Creates a LENGTH() function expression with the given argument. + * + * @param mixed $x Argument to be used as argument of LENGTH() function. + * @return Expr\Func A LENGTH function expression. + */ + public function length($x) + { + return new Expr\Func('LENGTH', array($x)); + } + + /** + * Creates a literal expression of the given argument. + * + * @param mixed $literal Argument to be converted to literal. + * @return Expr\Literal + */ + public function literal($literal) + { + return new Expr\Literal($this->_quoteLiteral($literal)); + } + + /** + * Quotes a literal value, if necessary, according to the DQL syntax. + * + * @param mixed $literal The literal value. + * @return string + */ + private function _quoteLiteral($literal) + { + if (is_numeric($literal) && !is_string($literal)) { + return (string) $literal; + } else { + return "'" . str_replace("'", "''", $literal) . "'"; + } + } + + /** + * Creates an instance of BETWEEN() function, with the given argument. + * + * @param mixed $val Valued to be inspected by range values. + * @param integer $x Starting range value to be used in BETWEEN() function. + * @param integer $y End point value to be used in BETWEEN() function. + * @return Expr\Func A BETWEEN expression. + */ + public function between($val, $x, $y) + { + return $val . ' BETWEEN ' . $x . ' AND ' . $y; + } + + /** + * Creates an instance of TRIM() function, with the given argument. + * + * @param mixed $x Argument to be used as argument of TRIM() function. + * @return Expr\Func a TRIM expression. + */ + public function trim($x) + { + return new Expr\Func('TRIM', $x); + } +} diff --git a/Doctrine/ORM/Query/Expr/Andx.php b/Doctrine/ORM/Query/Expr/Andx.php new file mode 100644 index 0000000..9f7d8a5 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Andx.php @@ -0,0 +1,43 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for building DQL and parts + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Andx extends Base +{ + protected $_separator = ') AND ('; + protected $_allowedClasses = array( + 'Doctrine\ORM\Query\Expr\Comparison', + 'Doctrine\ORM\Query\Expr\Func', + 'Doctrine\ORM\Query\Expr\Orx', + ); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Base.php b/Doctrine/ORM/Query/Expr/Base.php new file mode 100644 index 0000000..904b69b --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Base.php @@ -0,0 +1,85 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Abstract base Expr class for building DQL parts + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +abstract class Base +{ + protected $_preSeparator = '('; + protected $_separator = ', '; + protected $_postSeparator = ')'; + protected $_allowedClasses = array(); + + private $_parts = array(); + + public function __construct($args = array()) + { + $this->addMultiple($args); + } + + public function addMultiple($args = array()) + { + foreach ((array) $args as $arg) { + $this->add($arg); + } + } + + public function add($arg) + { + if ( ! empty($arg) || ($arg instanceof self && $arg->count() > 0)) { + // If we decide to keep Expr\Base instances, we can use this check + if ( ! is_string($arg)) { + $class = get_class($arg); + + if ( ! in_array($class, $this->_allowedClasses)) { + throw new \InvalidArgumentException("Expression of type '$class' not allowed in this context."); + } + } + + $this->_parts[] = $arg; + } + } + + public function count() + { + return count($this->_parts); + } + + public function __toString() + { + if ($this->count() == 1) { + return (string) $this->_parts[0]; + } + + return $this->_preSeparator . implode($this->_separator, $this->_parts) . $this->_postSeparator; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Comparison.php b/Doctrine/ORM/Query/Expr/Comparison.php new file mode 100644 index 0000000..7fcd263 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Comparison.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for DQL comparison expressions + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Comparison +{ + const EQ = '='; + const NEQ = '<>'; + const LT = '<'; + const LTE = '<='; + const GT = '>'; + const GTE = '>='; + + private $_leftExpr; + private $_operator; + private $_rightExpr; + + public function __construct($leftExpr, $operator, $rightExpr) + { + $this->_leftExpr = $leftExpr; + $this->_operator = $operator; + $this->_rightExpr = $rightExpr; + } + + public function __toString() + { + return $this->_leftExpr . ' ' . $this->_operator . ' ' . $this->_rightExpr; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/From.php b/Doctrine/ORM/Query/Expr/From.php new file mode 100644 index 0000000..6646e1d --- /dev/null +++ b/Doctrine/ORM/Query/Expr/From.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for DQL from + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class From +{ + private $_from; + private $_alias; + + public function __construct($from, $alias) + { + $this->_from = $from; + $this->_alias = $alias; + } + + public function getFrom() + { + return $this->_from; + } + + public function getAlias() + { + return $this->_alias; + } + + public function __toString() + { + return $this->_from . ' ' . $this->_alias; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Func.php b/Doctrine/ORM/Query/Expr/Func.php new file mode 100644 index 0000000..48b1a5b --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Func.php @@ -0,0 +1,50 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for generating DQL functions + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Func +{ + private $_name; + private $_arguments; + + public function __construct($name, $arguments) + { + $this->_name = $name; + $this->_arguments = (array) $arguments; + } + + public function __toString() + { + return $this->_name . '(' . implode(', ', $this->_arguments) . ')'; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/GroupBy.php b/Doctrine/ORM/Query/Expr/GroupBy.php new file mode 100644 index 0000000..dc36ba3 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/GroupBy.php @@ -0,0 +1,39 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for building DQL Group By parts + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class GroupBy extends Base +{ + protected $_preSeparator = ''; + protected $_postSeparator = ''; +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Join.php b/Doctrine/ORM/Query/Expr/Join.php new file mode 100644 index 0000000..a1a3955 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Join.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for DQL from + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Join +{ + const INNER_JOIN = 'INNER'; + const LEFT_JOIN = 'LEFT'; + + const ON = 'ON'; + const WITH = 'WITH'; + + private $_joinType; + private $_join; + private $_alias; + private $_conditionType; + private $_condition; + + public function __construct($joinType, $join, $alias = null, $conditionType = null, $condition = null) + { + $this->_joinType = $joinType; + $this->_join = $join; + $this->_alias = $alias; + $this->_conditionType = $conditionType; + $this->_condition = $condition; + } + + public function __toString() + { + return strtoupper($this->_joinType) . ' JOIN ' . $this->_join + . ($this->_alias ? ' ' . $this->_alias : '') + . ($this->_condition ? ' ' . strtoupper($this->_conditionType) . ' ' . $this->_condition : ''); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Literal.php b/Doctrine/ORM/Query/Expr/Literal.php new file mode 100644 index 0000000..c1dd5f7 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Literal.php @@ -0,0 +1,9 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for DQL math statements + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Math +{ + private $_leftExpr; + private $_operator; + private $_rightExpr; + + public function __construct($leftExpr, $operator, $rightExpr) + { + $this->_leftExpr = $leftExpr; + $this->_operator = $operator; + $this->_rightExpr = $rightExpr; + } + + public function __toString() + { + // Adjusting Left Expression + $leftExpr = (string) $this->_leftExpr; + + if ($this->_leftExpr instanceof Math) { + $leftExpr = '(' . $leftExpr . ')'; + } + + // Adjusting Right Expression + $rightExpr = (string) $this->_rightExpr; + + if ($this->_rightExpr instanceof Math) { + $rightExpr = '(' . $rightExpr . ')'; + } + + return $leftExpr . ' ' . $this->_operator . ' ' . $rightExpr; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/OrderBy.php b/Doctrine/ORM/Query/Expr/OrderBy.php new file mode 100644 index 0000000..a24e286 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/OrderBy.php @@ -0,0 +1,66 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for building DQL Order By parts + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class OrderBy +{ + protected $_preSeparator = ''; + protected $_separator = ', '; + protected $_postSeparator = ''; + protected $_allowedClasses = array(); + + private $_parts = array(); + + public function __construct($sort = null, $order = null) + { + if ($sort) { + $this->add($sort, $order); + } + } + + public function add($sort, $order = null) + { + $order = ! $order ? 'ASC' : $order; + $this->_parts[] = $sort . ' '. $order; + } + + public function count() + { + return count($this->_parts); + } + + public function __tostring() + { + return $this->_preSeparator . implode($this->_separator, $this->_parts) . $this->_postSeparator; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Orx.php b/Doctrine/ORM/Query/Expr/Orx.php new file mode 100644 index 0000000..fe695f4 --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Orx.php @@ -0,0 +1,43 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for building DQL OR clauses + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Orx extends Base +{ + protected $_separator = ') OR ('; + protected $_allowedClasses = array( + 'Doctrine\ORM\Query\Expr\Andx', + 'Doctrine\ORM\Query\Expr\Comparison', + 'Doctrine\ORM\Query\Expr\Func', + ); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Expr/Select.php b/Doctrine/ORM/Query/Expr/Select.php new file mode 100644 index 0000000..a310a0c --- /dev/null +++ b/Doctrine/ORM/Query/Expr/Select.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Query\Expr; + +/** + * Expression class for building DQL select statements + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Select extends Base +{ + protected $_preSeparator = ''; + protected $_postSeparator = ''; + protected $_allowedClasses = array( + 'Doctrine\ORM\Query\Expr\Func' + ); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Lexer.php b/Doctrine/ORM/Query/Lexer.php new file mode 100644 index 0000000..673ab02 --- /dev/null +++ b/Doctrine/ORM/Query/Lexer.php @@ -0,0 +1,196 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * Scans a DQL query for tokens. + * + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Roman Borschel + * @since 2.0 + */ +class Lexer extends \Doctrine\Common\Lexer +{ + // All tokens that are not valid identifiers must be < 100 + const T_NONE = 1; + const T_INTEGER = 2; + const T_STRING = 3; + const T_INPUT_PARAMETER = 4; + const T_FLOAT = 5; + const T_CLOSE_PARENTHESIS = 6; + const T_OPEN_PARENTHESIS = 7; + const T_COMMA = 8; + const T_DIVIDE = 9; + const T_DOT = 10; + const T_EQUALS = 11; + const T_GREATER_THAN = 12; + const T_LOWER_THAN = 13; + const T_MINUS = 14; + const T_MULTIPLY = 15; + const T_NEGATE = 16; + const T_PLUS = 17; + const T_OPEN_CURLY_BRACE = 18; + const T_CLOSE_CURLY_BRACE = 19; + + // All tokens that are also identifiers should be >= 100 + const T_IDENTIFIER = 100; + const T_ALL = 101; + const T_AND = 102; + const T_ANY = 103; + const T_AS = 104; + const T_ASC = 105; + const T_AVG = 106; + const T_BETWEEN = 107; + const T_BOTH = 108; + const T_BY = 109; + const T_CASE = 110; + const T_COALESCE = 111; + const T_COUNT = 112; + const T_DELETE = 113; + const T_DESC = 114; + const T_DISTINCT = 115; + const T_EMPTY = 116; + const T_ESCAPE = 117; + const T_EXISTS = 118; + const T_FALSE = 119; + const T_FROM = 120; + const T_GROUP = 121; + const T_HAVING = 122; + const T_IN = 123; + const T_INDEX = 124; + const T_INNER = 125; + const T_INSTANCE = 126; + const T_IS = 127; + const T_JOIN = 128; + const T_LEADING = 129; + const T_LEFT = 130; + const T_LIKE = 131; + const T_MAX = 132; + const T_MEMBER = 133; + const T_MIN = 134; + const T_NOT = 135; + const T_NULL = 136; + const T_NULLIF = 137; + const T_OF = 138; + const T_OR = 139; + const T_ORDER = 140; + const T_OUTER = 141; + const T_SELECT = 142; + const T_SET = 143; + const T_SIZE = 144; + const T_SOME = 145; + const T_SUM = 146; + const T_TRAILING = 147; + const T_TRUE = 148; + const T_UPDATE = 149; + const T_WHEN = 150; + const T_WHERE = 151; + const T_WITH = 153; + const T_PARTIAL = 154; + const T_MOD = 155; + + /** + * Creates a new query scanner object. + * + * @param string $input a query string + */ + public function __construct($input) + { + $this->setInput($input); + } + + /** + * @inheritdoc + */ + protected function getCatchablePatterns() + { + return array( + '[a-z_\\\][a-z0-9_\:\\\]*[a-z0-9_]{1}', + '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', + "'(?:[^']|'')*'", + '\?[1-9][0-9]*|:[a-z][a-z0-9_]+' + ); + } + + /** + * @inheritdoc + */ + protected function getNonCatchablePatterns() + { + return array('\s+', '(.)'); + } + + /** + * @inheritdoc + */ + protected function getType(&$value) + { + $type = self::T_NONE; + + // Recognizing numeric values + if (is_numeric($value)) { + return (strpos($value, '.') !== false || stripos($value, 'e') !== false) + ? self::T_FLOAT : self::T_INTEGER; + } + + // Differentiate between quoted names, identifiers, input parameters and symbols + if ($value[0] === "'") { + $value = str_replace("''", "'", substr($value, 1, strlen($value) - 2)); + return self::T_STRING; + } else if (ctype_alpha($value[0]) || $value[0] === '_') { + $name = 'Doctrine\ORM\Query\Lexer::T_' . strtoupper($value); + + if (defined($name)) { + $type = constant($name); + + if ($type > 100) { + return $type; + } + } + + return self::T_IDENTIFIER; + } else if ($value[0] === '?' || $value[0] === ':') { + return self::T_INPUT_PARAMETER; + } else { + switch ($value) { + case '.': return self::T_DOT; + case ',': return self::T_COMMA; + case '(': return self::T_OPEN_PARENTHESIS; + case ')': return self::T_CLOSE_PARENTHESIS; + case '=': return self::T_EQUALS; + case '>': return self::T_GREATER_THAN; + case '<': return self::T_LOWER_THAN; + case '+': return self::T_PLUS; + case '-': return self::T_MINUS; + case '*': return self::T_MULTIPLY; + case '/': return self::T_DIVIDE; + case '!': return self::T_NEGATE; + case '{': return self::T_OPEN_CURLY_BRACE; + case '}': return self::T_CLOSE_CURLY_BRACE; + default: + // Do nothing + break; + } + } + + return $type; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Parser.php b/Doctrine/ORM/Query/Parser.php new file mode 100644 index 0000000..e5b9f69 --- /dev/null +++ b/Doctrine/ORM/Query/Parser.php @@ -0,0 +1,2764 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\ORM\Query; +use Doctrine\ORM\Mapping\ClassMetadata; + +/** + * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. + * Parses a DQL query, reports any errors in it, and generates an AST. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Janne Vanhala + */ +class Parser +{ + /** READ-ONLY: Maps BUILT-IN string function names to AST class names. */ + private static $_STRING_FUNCTIONS = array( + 'concat' => 'Doctrine\ORM\Query\AST\Functions\ConcatFunction', + 'substring' => 'Doctrine\ORM\Query\AST\Functions\SubstringFunction', + 'trim' => 'Doctrine\ORM\Query\AST\Functions\TrimFunction', + 'lower' => 'Doctrine\ORM\Query\AST\Functions\LowerFunction', + 'upper' => 'Doctrine\ORM\Query\AST\Functions\UpperFunction' + ); + + /** READ-ONLY: Maps BUILT-IN numeric function names to AST class names. */ + private static $_NUMERIC_FUNCTIONS = array( + 'length' => 'Doctrine\ORM\Query\AST\Functions\LengthFunction', + 'locate' => 'Doctrine\ORM\Query\AST\Functions\LocateFunction', + 'abs' => 'Doctrine\ORM\Query\AST\Functions\AbsFunction', + 'sqrt' => 'Doctrine\ORM\Query\AST\Functions\SqrtFunction', + 'mod' => 'Doctrine\ORM\Query\AST\Functions\ModFunction', + 'size' => 'Doctrine\ORM\Query\AST\Functions\SizeFunction' + ); + + /** READ-ONLY: Maps BUILT-IN datetime function names to AST class names. */ + private static $_DATETIME_FUNCTIONS = array( + 'current_date' => 'Doctrine\ORM\Query\AST\Functions\CurrentDateFunction', + 'current_time' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimeFunction', + 'current_timestamp' => 'Doctrine\ORM\Query\AST\Functions\CurrentTimestampFunction' + ); + + /** + * Expressions that were encountered during parsing of identifiers and expressions + * and still need to be validated. + */ + private $_deferredIdentificationVariables = array(); + private $_deferredPartialObjectExpressions = array(); + private $_deferredPathExpressions = array(); + private $_deferredResultVariables = array(); + + /** + * The lexer. + * + * @var Doctrine\ORM\Query\Lexer + */ + private $_lexer; + + /** + * The parser result. + * + * @var Doctrine\ORM\Query\ParserResult + */ + private $_parserResult; + + /** + * The EntityManager. + * + * @var EnityManager + */ + private $_em; + + /** + * The Query to parse. + * + * @var Query + */ + private $_query; + + /** + * Map of declared query components in the parsed query. + * + * @var array + */ + private $_queryComponents = array(); + + /** + * Keeps the nesting level of defined ResultVariables + * + * @var integer + */ + private $_nestingLevel = 0; + + /** + * Any additional custom tree walkers that modify the AST. + * + * @var array + */ + private $_customTreeWalkers = array(); + + /** + * The custom last tree walker, if any, that is responsible for producing the output. + * + * @var TreeWalker + */ + private $_customOutputWalker; + + /** + * @var array + */ + private $_identVariableExpressions = array(); + + /** + * Creates a new query parser object. + * + * @param Query $query The Query to parse. + */ + public function __construct(Query $query) + { + $this->_query = $query; + $this->_em = $query->getEntityManager(); + $this->_lexer = new Lexer($query->getDql()); + $this->_parserResult = new ParserResult(); + } + + /** + * Sets a custom tree walker that produces output. + * This tree walker will be run last over the AST, after any other walkers. + * + * @param string $className + */ + public function setCustomOutputTreeWalker($className) + { + $this->_customOutputWalker = $className; + } + + /** + * Adds a custom tree walker for modifying the AST. + * + * @param string $className + */ + public function addCustomTreeWalker($className) + { + $this->_customTreeWalkers[] = $className; + } + + /** + * Gets the lexer used by the parser. + * + * @return Doctrine\ORM\Query\Lexer + */ + public function getLexer() + { + return $this->_lexer; + } + + /** + * Gets the ParserResult that is being filled with information during parsing. + * + * @return Doctrine\ORM\Query\ParserResult + */ + public function getParserResult() + { + return $this->_parserResult; + } + + /** + * Gets the EntityManager used by the parser. + * + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /** + * Parse and build AST for the given Query. + * + * @return \Doctrine\ORM\Query\AST\SelectStatement | + * \Doctrine\ORM\Query\AST\UpdateStatement | + * \Doctrine\ORM\Query\AST\DeleteStatement + */ + public function getAST() + { + // Parse & build AST + $AST = $this->QueryLanguage(); + + // Process any deferred validations of some nodes in the AST. + // This also allows post-processing of the AST for modification purposes. + $this->_processDeferredIdentificationVariables(); + + if ($this->_deferredPartialObjectExpressions) { + $this->_processDeferredPartialObjectExpressions(); + } + + if ($this->_deferredPathExpressions) { + $this->_processDeferredPathExpressions($AST); + } + + if ($this->_deferredResultVariables) { + $this->_processDeferredResultVariables(); + } + + return $AST; + } + + /** + * Attempts to match the given token with the current lookahead token. + * + * If they match, updates the lookahead token; otherwise raises a syntax + * error. + * + * @param int|string token type or value + * @return void + * @throws QueryException If the tokens dont match. + */ + public function match($token) + { + // short-circuit on first condition, usually types match + if ($this->_lexer->lookahead['type'] !== $token && + $token !== Lexer::T_IDENTIFIER && + $this->_lexer->lookahead['type'] <= Lexer::T_IDENTIFIER + ) { + $this->syntaxError($this->_lexer->getLiteral($token)); + } + + $this->_lexer->moveNext(); + } + + /** + * Free this parser enabling it to be reused + * + * @param boolean $deep Whether to clean peek and reset errors + * @param integer $position Position to reset + */ + public function free($deep = false, $position = 0) + { + // WARNING! Use this method with care. It resets the scanner! + $this->_lexer->resetPosition($position); + + // Deep = true cleans peek and also any previously defined errors + if ($deep) { + $this->_lexer->resetPeek(); + } + + $this->_lexer->token = null; + $this->_lexer->lookahead = null; + } + + /** + * Parses a query string. + * + * @return ParserResult + */ + public function parse() + { + $AST = $this->getAST(); + + if (($customWalkers = $this->_query->getHint(Query::HINT_CUSTOM_TREE_WALKERS)) !== false) { + $this->_customTreeWalkers = $customWalkers; + } + + if (($customOutputWalker = $this->_query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER)) !== false) { + $this->_customOutputWalker = $customOutputWalker; + } + + // Run any custom tree walkers over the AST + if ($this->_customTreeWalkers) { + $treeWalkerChain = new TreeWalkerChain($this->_query, $this->_parserResult, $this->_queryComponents); + + foreach ($this->_customTreeWalkers as $walker) { + $treeWalkerChain->addTreeWalker($walker); + } + + if ($AST instanceof AST\SelectStatement) { + $treeWalkerChain->walkSelectStatement($AST); + } else if ($AST instanceof AST\UpdateStatement) { + $treeWalkerChain->walkUpdateStatement($AST); + } else { + $treeWalkerChain->walkDeleteStatement($AST); + } + } + + // Fix order of identification variables. + // They have to appear in the select clause in the same order as the + // declarations (from ... x join ... y join ... z ...) appear in the query + // as the hydration process relies on that order for proper operation. + if ( count($this->_identVariableExpressions) > 1) { + foreach ($this->_queryComponents as $dqlAlias => $qComp) { + if (isset($this->_identVariableExpressions[$dqlAlias])) { + $expr = $this->_identVariableExpressions[$dqlAlias]; + $key = array_search($expr, $AST->selectClause->selectExpressions); + unset($AST->selectClause->selectExpressions[$key]); + $AST->selectClause->selectExpressions[] = $expr; + } + } + } + + if ($this->_customOutputWalker) { + $outputWalker = new $this->_customOutputWalker( + $this->_query, $this->_parserResult, $this->_queryComponents + ); + } else { + $outputWalker = new SqlWalker( + $this->_query, $this->_parserResult, $this->_queryComponents + ); + } + + // Assign an SQL executor to the parser result + $this->_parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); + + return $this->_parserResult; + } + + /** + * Generates a new syntax error. + * + * @param string $expected Expected string. + * @param array $token Got token. + * + * @throws \Doctrine\ORM\Query\QueryException + */ + public function syntaxError($expected = '', $token = null) + { + if ($token === null) { + $token = $this->_lexer->lookahead; + } + + $tokenPos = (isset($token['position'])) ? $token['position'] : '-1'; + $message = "line 0, col {$tokenPos}: Error: "; + + if ($expected !== '') { + $message .= "Expected {$expected}, got "; + } else { + $message .= 'Unexpected '; + } + + if ($this->_lexer->lookahead === null) { + $message .= 'end of string.'; + } else { + $message .= "'{$token['value']}'"; + } + + throw QueryException::syntaxError($message); + } + + /** + * Generates a new semantical error. + * + * @param string $message Optional message. + * @param array $token Optional token. + * + * @throws \Doctrine\ORM\Query\QueryException + */ + public function semanticalError($message = '', $token = null) + { + if ($token === null) { + $token = $this->_lexer->lookahead; + } + + // Minimum exposed chars ahead of token + $distance = 12; + + // Find a position of a final word to display in error string + $dql = $this->_query->getDql(); + $length = strlen($dql); + $pos = $token['position'] + $distance; + $pos = strpos($dql, ' ', ($length > $pos) ? $pos : $length); + $length = ($pos !== false) ? $pos - $token['position'] : $distance; + + // Building informative message + $message = 'line 0, col ' . ( + (isset($token['position']) && $token['position'] > 0) ? $token['position'] : '-1' + ) . " near '" . substr($dql, $token['position'], $length) . "': Error: " . $message; + + throw \Doctrine\ORM\Query\QueryException::semanticalError($message); + } + + /** + * Peeks beyond the specified token and returns the first token after that one. + * + * @param array $token + * @return array + */ + private function _peekBeyond($token) + { + $peek = $this->_lexer->peek(); + + while ($peek['value'] != $token) { + $peek = $this->_lexer->peek(); + } + + $peek = $this->_lexer->peek(); + $this->_lexer->resetPeek(); + + return $peek; + } + + /** + * Peek beyond the matched closing parenthesis and return the first token after that one. + * + * @return array + */ + private function _peekBeyondClosingParenthesis() + { + $token = $this->_lexer->peek(); + $numUnmatched = 1; + + while ($numUnmatched > 0 && $token !== null) { + if ($token['value'] == ')') { + --$numUnmatched; + } else if ($token['value'] == '(') { + ++$numUnmatched; + } + + $token = $this->_lexer->peek(); + } + + $this->_lexer->resetPeek(); + + return $token; + } + + /** + * Checks if the given token indicates a mathematical operator. + * + * @return boolean TRUE if the token is a mathematical operator, FALSE otherwise. + */ + private function _isMathOperator($token) + { + return in_array($token['value'], array("+", "-", "/", "*")); + } + + /** + * Checks if the next-next (after lookahead) token starts a function. + * + * @return boolean TRUE if the next-next tokens start a function, FALSE otherwise. + */ + private function _isFunction() + { + $peek = $this->_lexer->peek(); + $nextpeek = $this->_lexer->peek(); + $this->_lexer->resetPeek(); + + // We deny the COUNT(SELECT * FROM User u) here. COUNT won't be considered a function + return ($peek['value'] === '(' && $nextpeek['type'] !== Lexer::T_SELECT); + } + + /** + * Checks whether the given token type indicates an aggregate function. + * + * @return boolean TRUE if the token type is an aggregate function, FALSE otherwise. + */ + private function _isAggregateFunction($tokenType) + { + return $tokenType == Lexer::T_AVG || $tokenType == Lexer::T_MIN || + $tokenType == Lexer::T_MAX || $tokenType == Lexer::T_SUM || + $tokenType == Lexer::T_COUNT; + } + + /** + * Checks whether the current lookahead token of the lexer has the type + * T_ALL, T_ANY or T_SOME. + * + * @return boolean + */ + private function _isNextAllAnySome() + { + return $this->_lexer->lookahead['type'] === Lexer::T_ALL || + $this->_lexer->lookahead['type'] === Lexer::T_ANY || + $this->_lexer->lookahead['type'] === Lexer::T_SOME; + } + + /** + * Checks whether the next 2 tokens start a subselect. + * + * @return boolean TRUE if the next 2 tokens start a subselect, FALSE otherwise. + */ + private function _isSubselect() + { + $la = $this->_lexer->lookahead; + $next = $this->_lexer->glimpse(); + + return ($la['value'] === '(' && $next['type'] === Lexer::T_SELECT); + } + + /** + * Validates that the given IdentificationVariable is semantically correct. + * It must exist in query components list. + * + * @return void + */ + private function _processDeferredIdentificationVariables() + { + foreach ($this->_deferredIdentificationVariables as $deferredItem) { + $identVariable = $deferredItem['expression']; + + // Check if IdentificationVariable exists in queryComponents + if ( ! isset($this->_queryComponents[$identVariable])) { + $this->semanticalError( + "'$identVariable' is not defined.", $deferredItem['token'] + ); + } + + $qComp = $this->_queryComponents[$identVariable]; + + // Check if queryComponent points to an AbstractSchemaName or a ResultVariable + if ( ! isset($qComp['metadata'])) { + $this->semanticalError( + "'$identVariable' does not point to a Class.", $deferredItem['token'] + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( + "'$identVariable' is used outside the scope of its declaration.", $deferredItem['token'] + ); + } + } + } + + /** + * Validates that the given PartialObjectExpression is semantically correct. + * It must exist in query components list. + * + * @return void + */ + private function _processDeferredPartialObjectExpressions() + { + foreach ($this->_deferredPartialObjectExpressions as $deferredItem) { + $expr = $deferredItem['expression']; + $class = $this->_queryComponents[$expr->identificationVariable]['metadata']; + + foreach ($expr->partialFieldSet as $field) { + if ( ! isset($class->fieldMappings[$field])) { + $this->semanticalError( + "There is no mapped field named '$field' on class " . $class->name . ".", + $deferredItem['token'] + ); + } + } + + if (array_intersect($class->identifier, $expr->partialFieldSet) != $class->identifier) { + $this->semanticalError( + "The partial field selection of class " . $class->name . " must contain the identifier.", + $deferredItem['token'] + ); + } + } + } + + /** + * Validates that the given ResultVariable is semantically correct. + * It must exist in query components list. + * + * @return void + */ + private function _processDeferredResultVariables() + { + foreach ($this->_deferredResultVariables as $deferredItem) { + $resultVariable = $deferredItem['expression']; + + // Check if ResultVariable exists in queryComponents + if ( ! isset($this->_queryComponents[$resultVariable])) { + $this->semanticalError( + "'$resultVariable' is not defined.", $deferredItem['token'] + ); + } + + $qComp = $this->_queryComponents[$resultVariable]; + + // Check if queryComponent points to an AbstractSchemaName or a ResultVariable + if ( ! isset($qComp['resultVariable'])) { + $this->semanticalError( + "'$identVariable' does not point to a ResultVariable.", $deferredItem['token'] + ); + } + + // Validate if identification variable nesting level is lower or equal than the current one + if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { + $this->semanticalError( + "'$resultVariable' is used outside the scope of its declaration.", $deferredItem['token'] + ); + } + } + } + + /** + * Validates that the given PathExpression is semantically correct for grammar rules: + * + * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression + * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression + * StateFieldPathExpression ::= IdentificationVariable "." StateField + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField + * + * @param array $deferredItem + * @param mixed $AST + */ + private function _processDeferredPathExpressions($AST) + { + foreach ($this->_deferredPathExpressions as $deferredItem) { + $pathExpression = $deferredItem['expression']; + + $qComp = $this->_queryComponents[$pathExpression->identificationVariable]; + $class = $qComp['metadata']; + + if (($field = $pathExpression->field) === null) { + $field = $pathExpression->field = $class->identifier[0]; + } + + // Check if field or association exists + if ( ! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { + $this->semanticalError( + 'Class ' . $class->name . ' has no field or association named ' . $field, + $deferredItem['token'] + ); + } + + if (isset($class->fieldMappings[$field])) { + $fieldType = AST\PathExpression::TYPE_STATE_FIELD; + } else { + $assoc = $class->associationMappings[$field]; + $class = $this->_em->getClassMetadata($assoc['targetEntity']); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $fieldType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + } else { + $fieldType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; + } + } + + // Validate if PathExpression is one of the expected types + $expectedType = $pathExpression->expectedType; + + if ( ! ($expectedType & $fieldType)) { + // We need to recognize which was expected type(s) + $expectedStringTypes = array(); + + // Validate state field type + if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { + $expectedStringTypes[] = 'StateFieldPathExpression'; + } + + // Validate single valued association (*-to-one) + if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'SingleValuedAssociationField'; + } + + // Validate single valued association (*-to-many) + if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { + $expectedStringTypes[] = 'CollectionValuedAssociationField'; + } + + // Build the error message + $semanticalError = 'Invalid PathExpression. '; + + if (count($expectedStringTypes) == 1) { + $semanticalError .= 'Must be a ' . $expectedStringTypes[0] . '.'; + } else { + $semanticalError .= implode(' or ', $expectedStringTypes) . ' expected.'; + } + + $this->semanticalError($semanticalError, $deferredItem['token']); + } + + // We need to force the type in PathExpression + $pathExpression->type = $fieldType; + } + } + + /** + * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement + * + * @return \Doctrine\ORM\Query\AST\SelectStatement | + * \Doctrine\ORM\Query\AST\UpdateStatement | + * \Doctrine\ORM\Query\AST\DeleteStatement + */ + public function QueryLanguage() + { + $this->_lexer->moveNext(); + + switch ($this->_lexer->lookahead['type']) { + case Lexer::T_SELECT: + $statement = $this->SelectStatement(); + break; + case Lexer::T_UPDATE: + $statement = $this->UpdateStatement(); + break; + case Lexer::T_DELETE: + $statement = $this->DeleteStatement(); + break; + default: + $this->syntaxError('SELECT, UPDATE or DELETE'); + break; + } + + // Check for end of string + if ($this->_lexer->lookahead !== null) { + $this->syntaxError('end of string'); + } + + return $statement; + } + + /** + * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + * + * @return \Doctrine\ORM\Query\AST\SelectStatement + */ + public function SelectStatement() + { + $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); + + $selectStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) + ? $this->WhereClause() : null; + + $selectStatement->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) + ? $this->GroupByClause() : null; + + $selectStatement->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) + ? $this->HavingClause() : null; + + $selectStatement->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) + ? $this->OrderByClause() : null; + + return $selectStatement; + } + + /** + * UpdateStatement ::= UpdateClause [WhereClause] + * + * @return \Doctrine\ORM\Query\AST\UpdateStatement + */ + public function UpdateStatement() + { + $updateStatement = new AST\UpdateStatement($this->UpdateClause()); + $updateStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) + ? $this->WhereClause() : null; + + return $updateStatement; + } + + /** + * DeleteStatement ::= DeleteClause [WhereClause] + * + * @return \Doctrine\ORM\Query\AST\DeleteStatement + */ + public function DeleteStatement() + { + $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); + $deleteStatement->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) + ? $this->WhereClause() : null; + + return $deleteStatement; + } + + /** + * IdentificationVariable ::= identifier + * + * @return string + */ + public function IdentificationVariable() + { + $this->match(Lexer::T_IDENTIFIER); + + $identVariable = $this->_lexer->token['value']; + + $this->_deferredIdentificationVariables[] = array( + 'expression' => $identVariable, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, + ); + + return $identVariable; + } + + /** + * AliasIdentificationVariable = identifier + * + * @return string + */ + public function AliasIdentificationVariable() + { + $this->match(Lexer::T_IDENTIFIER); + + $aliasIdentVariable = $this->_lexer->token['value']; + $exists = isset($this->_queryComponents[$aliasIdentVariable]); + + if ($exists) { + $this->semanticalError( + "'$aliasIdentVariable' is already defined.", $this->_lexer->token + ); + } + + return $aliasIdentVariable; + } + + /** + * AbstractSchemaName ::= identifier + * + * @return string + */ + public function AbstractSchemaName() + { + $this->match(Lexer::T_IDENTIFIER); + + $schemaName = ltrim($this->_lexer->token['value'], '\\'); + + if (strrpos($schemaName, ':') !== false) { + list($namespaceAlias, $simpleClassName) = explode(':', $schemaName); + $schemaName = $this->_em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName; + } + + $exists = class_exists($schemaName, true); + + if ( ! $exists) { + $this->semanticalError("Class '$schemaName' is not defined.", $this->_lexer->token); + } + + return $schemaName; + } + + /** + * AliasResultVariable ::= identifier + * + * @return string + */ + public function AliasResultVariable() + { + $this->match(Lexer::T_IDENTIFIER); + + $resultVariable = $this->_lexer->token['value']; + $exists = isset($this->_queryComponents[$resultVariable]); + + if ($exists) { + $this->semanticalError( + "'$resultVariable' is already defined.", $this->_lexer->token + ); + } + + return $resultVariable; + } + + /** + * ResultVariable ::= identifier + * + * @return string + */ + public function ResultVariable() + { + $this->match(Lexer::T_IDENTIFIER); + + $resultVariable = $this->_lexer->token['value']; + + // Defer ResultVariable validation + $this->_deferredResultVariables[] = array( + 'expression' => $resultVariable, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, + ); + + return $resultVariable; + } + + /** + * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) + * + * @return \Doctrine\ORM\Query\AST\JoinAssociationPathExpression + */ + public function JoinAssociationPathExpression() + { + $token = $this->_lexer->lookahead; + $identVariable = $this->IdentificationVariable(); + + $this->match(Lexer::T_DOT); + $this->match(Lexer::T_IDENTIFIER); + + $field = $this->_lexer->token['value']; + + // Validate association field + $qComp = $this->_queryComponents[$identVariable]; + $class = $qComp['metadata']; + + if ( ! isset($class->associationMappings[$field])) { + $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); + } + + return new AST\JoinAssociationPathExpression($identVariable, $field); + } + + /** + * Parses an arbitrary path expression and defers semantical validation + * based on expected types. + * + * PathExpression ::= IdentificationVariable "." identifier + * + * @param integer $expectedTypes + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function PathExpression($expectedTypes) + { + $token = $this->_lexer->lookahead; + $identVariable = $this->IdentificationVariable(); + $field = null; + + if ($this->_lexer->isNextToken(Lexer::T_DOT)) { + $this->match(Lexer::T_DOT); + $this->match(Lexer::T_IDENTIFIER); + + $field = $this->_lexer->token['value']; + } + + // Creating AST node + $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); + + // Defer PathExpression validation if requested to be defered + $this->_deferredPathExpressions[] = array( + 'expression' => $pathExpr, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, + ); + + return $pathExpr; + } + + /** + * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression + * + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function AssociationPathExpression() + { + return $this->PathExpression( + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | + AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION + ); + } + + /** + * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression + * + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function SingleValuedPathExpression() + { + return $this->PathExpression( + AST\PathExpression::TYPE_STATE_FIELD | + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION + ); + } + + /** + * StateFieldPathExpression ::= IdentificationVariable "." StateField + * + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function StateFieldPathExpression() + { + return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); + } + + /** + * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + * + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function SingleValuedAssociationPathExpression() + { + return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); + } + + /** + * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField + * + * @return \Doctrine\ORM\Query\AST\PathExpression + */ + public function CollectionValuedPathExpression() + { + return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); + } + + /** + * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} + * + * @return \Doctrine\ORM\Query\AST\SelectClause + */ + public function SelectClause() + { + $isDistinct = false; + $this->match(Lexer::T_SELECT); + + // Check for DISTINCT + if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { + $this->match(Lexer::T_DISTINCT); + $isDistinct = true; + } + + // Process SelectExpressions (1..N) + $selectExpressions = array(); + $selectExpressions[] = $this->SelectExpression(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $selectExpressions[] = $this->SelectExpression(); + } + + return new AST\SelectClause($selectExpressions, $isDistinct); + } + + /** + * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression + * + * @return \Doctrine\ORM\Query\AST\SimpleSelectClause + */ + public function SimpleSelectClause() + { + $isDistinct = false; + $this->match(Lexer::T_SELECT); + + if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { + $this->match(Lexer::T_DISTINCT); + $isDistinct = true; + } + + return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct); + } + + /** + * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}* + * + * @return \Doctrine\ORM\Query\AST\UpdateClause + */ + public function UpdateClause() + { + $this->match(Lexer::T_UPDATE); + $token = $this->_lexer->lookahead; + $abstractSchemaName = $this->AbstractSchemaName(); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + $class = $this->_em->getClassMetadata($abstractSchemaName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $class, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; + + $this->match(Lexer::T_SET); + + $updateItems = array(); + $updateItems[] = $this->UpdateItem(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $updateItems[] = $this->UpdateItem(); + } + + $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems); + $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable; + + return $updateClause; + } + + /** + * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable + * + * @return \Doctrine\ORM\Query\AST\DeleteClause + */ + public function DeleteClause() + { + $this->match(Lexer::T_DELETE); + + if ($this->_lexer->isNextToken(Lexer::T_FROM)) { + $this->match(Lexer::T_FROM); + } + + $token = $this->_lexer->lookahead; + $deleteClause = new AST\DeleteClause($this->AbstractSchemaName()); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable; + $class = $this->_em->getClassMetadata($deleteClause->abstractSchemaName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $class, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; + + return $deleteClause; + } + + /** + * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* + * + * @return \Doctrine\ORM\Query\AST\FromClause + */ + public function FromClause() + { + $this->match(Lexer::T_FROM); + $identificationVariableDeclarations = array(); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); + } + + return new AST\FromClause($identificationVariableDeclarations); + } + + /** + * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* + * + * @return \Doctrine\ORM\Query\AST\SubselectFromClause + */ + public function SubselectFromClause() + { + $this->match(Lexer::T_FROM); + $identificationVariables = array(); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); + } + + return new AST\SubselectFromClause($identificationVariables); + } + + /** + * WhereClause ::= "WHERE" ConditionalExpression + * + * @return \Doctrine\ORM\Query\AST\WhereClause + */ + public function WhereClause() + { + $this->match(Lexer::T_WHERE); + + return new AST\WhereClause($this->ConditionalExpression()); + } + + /** + * HavingClause ::= "HAVING" ConditionalExpression + * + * @return \Doctrine\ORM\Query\AST\HavingClause + */ + public function HavingClause() + { + $this->match(Lexer::T_HAVING); + + return new AST\HavingClause($this->ConditionalExpression()); + } + + /** + * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* + * + * @return \Doctrine\ORM\Query\AST\GroupByClause + */ + public function GroupByClause() + { + $this->match(Lexer::T_GROUP); + $this->match(Lexer::T_BY); + + $groupByItems = array($this->GroupByItem()); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $groupByItems[] = $this->GroupByItem(); + } + + return new AST\GroupByClause($groupByItems); + } + + /** + * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* + * + * @return \Doctrine\ORM\Query\AST\OrderByClause + */ + public function OrderByClause() + { + $this->match(Lexer::T_ORDER); + $this->match(Lexer::T_BY); + + $orderByItems = array(); + $orderByItems[] = $this->OrderByItem(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $orderByItems[] = $this->OrderByItem(); + } + + return new AST\OrderByClause($orderByItems); + } + + /** + * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] + * + * @return \Doctrine\ORM\Query\AST\Subselect + */ + public function Subselect() + { + // Increase query nesting level + $this->_nestingLevel++; + + $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); + + $subselect->whereClause = $this->_lexer->isNextToken(Lexer::T_WHERE) + ? $this->WhereClause() : null; + + $subselect->groupByClause = $this->_lexer->isNextToken(Lexer::T_GROUP) + ? $this->GroupByClause() : null; + + $subselect->havingClause = $this->_lexer->isNextToken(Lexer::T_HAVING) + ? $this->HavingClause() : null; + + $subselect->orderByClause = $this->_lexer->isNextToken(Lexer::T_ORDER) + ? $this->OrderByClause() : null; + + // Decrease query nesting level + $this->_nestingLevel--; + + return $subselect; + } + + /** + * UpdateItem ::= SingleValuedPathExpression "=" NewValue + * + * @return \Doctrine\ORM\Query\AST\UpdateItem + */ + public function UpdateItem() + { + $pathExpr = $this->SingleValuedPathExpression(); + + $this->match(Lexer::T_EQUALS); + + $updateItem = new AST\UpdateItem($pathExpr, $this->NewValue()); + + return $updateItem; + } + + /** + * GroupByItem ::= IdentificationVariable | SingleValuedPathExpression + * + * @return string | \Doctrine\ORM\Query\AST\PathExpression + */ + public function GroupByItem() + { + // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression + $glimpse = $this->_lexer->glimpse(); + + if ($glimpse['type'] != Lexer::T_DOT) { + $token = $this->_lexer->lookahead; + $identVariable = $this->IdentificationVariable(); + + return $identVariable; + } + + return $this->SingleValuedPathExpression(); + } + + /** + * OrderByItem ::= (ResultVariable | StateFieldPathExpression) ["ASC" | "DESC"] + * + * @todo Post 2.0 release. Support general SingleValuedPathExpression instead + * of only StateFieldPathExpression. + * + * @return \Doctrine\ORM\Query\AST\OrderByItem + */ + public function OrderByItem() + { + $type = 'ASC'; + + // We need to check if we are in a ResultVariable or StateFieldPathExpression + $glimpse = $this->_lexer->glimpse(); + + if ($glimpse['type'] != Lexer::T_DOT) { + $token = $this->_lexer->lookahead; + $expr = $this->ResultVariable(); + } else { + $expr = $this->StateFieldPathExpression(); + } + + $item = new AST\OrderByItem($expr); + + if ($this->_lexer->isNextToken(Lexer::T_ASC)) { + $this->match(Lexer::T_ASC); + } else if ($this->_lexer->isNextToken(Lexer::T_DESC)) { + $this->match(Lexer::T_DESC); + $type = 'DESC'; + } + + $item->type = $type; + return $item; + } + + /** + * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | + * EnumPrimary | SimpleEntityExpression | "NULL" + * + * NOTE: Since it is not possible to correctly recognize individual types, here is the full + * grammar that needs to be supported: + * + * NewValue ::= SimpleArithmeticExpression | "NULL" + * + * SimpleArithmeticExpression covers all *Primary grammar rules and also SimplEntityExpression + */ + public function NewValue() + { + if ($this->_lexer->isNextToken(Lexer::T_NULL)) { + $this->match(Lexer::T_NULL); + return null; + } else if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + $this->match(Lexer::T_INPUT_PARAMETER); + return new AST\InputParameter($this->_lexer->token['value']); + } + + return $this->SimpleArithmeticExpression(); + } + + /** + * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* + * + * @return \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration + */ + public function IdentificationVariableDeclaration() + { + $rangeVariableDeclaration = $this->RangeVariableDeclaration(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; + $joinVariableDeclarations = array(); + + while ( + $this->_lexer->isNextToken(Lexer::T_LEFT) || + $this->_lexer->isNextToken(Lexer::T_INNER) || + $this->_lexer->isNextToken(Lexer::T_JOIN) + ) { + $joinVariableDeclarations[] = $this->JoinVariableDeclaration(); + } + + return new AST\IdentificationVariableDeclaration( + $rangeVariableDeclaration, $indexBy, $joinVariableDeclarations + ); + } + + /** + * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) + * + * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration | + * \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration + */ + public function SubselectIdentificationVariableDeclaration() + { + $glimpse = $this->_lexer->glimpse(); + + /* NOT YET IMPLEMENTED! + + if ($glimpse['type'] == Lexer::T_DOT) { + $subselectIdVarDecl = new AST\SubselectIdentificationVariableDeclaration(); + $subselectIdVarDecl->associationPathExpression = $this->AssociationPathExpression(); + $this->match(Lexer::T_AS); + $subselectIdVarDecl->aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + return $subselectIdVarDecl; + } + */ + + return $this->IdentificationVariableDeclaration(); + } + + /** + * JoinVariableDeclaration ::= Join [IndexBy] + * + * @return \Doctrine\ORM\Query\AST\JoinVariableDeclaration + */ + public function JoinVariableDeclaration() + { + $join = $this->Join(); + $indexBy = $this->_lexer->isNextToken(Lexer::T_INDEX) + ? $this->IndexBy() : null; + + return new AST\JoinVariableDeclaration($join, $indexBy); + } + + /** + * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable + * + * @return Doctrine\ORM\Query\AST\RangeVariableDeclaration + */ + public function RangeVariableDeclaration() + { + $abstractSchemaName = $this->AbstractSchemaName(); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $token = $this->_lexer->lookahead; + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); + + // Building queryComponent + $queryComponent = array( + 'metadata' => $classMetadata, + 'parent' => null, + 'relation' => null, + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token + ); + $this->_queryComponents[$aliasIdentificationVariable] = $queryComponent; + + return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); + } + + /** + * PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet + * PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" + * + * @return array + */ + public function PartialObjectExpression() + { + $this->match(Lexer::T_PARTIAL); + + $partialFieldSet = array(); + + $identificationVariable = $this->IdentificationVariable(); + $this->match(Lexer::T_DOT); + + $this->match(Lexer::T_OPEN_CURLY_BRACE); + $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $this->match(Lexer::T_IDENTIFIER); + $partialFieldSet[] = $this->_lexer->token['value']; + } + + $this->match(Lexer::T_CLOSE_CURLY_BRACE); + + $partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet); + + // Defer PartialObjectExpression validation + $this->_deferredPartialObjectExpressions[] = array( + 'expression' => $partialObjectExpression, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $this->_lexer->token, + ); + + return $partialObjectExpression; + } + + /** + * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression + * ["AS"] AliasIdentificationVariable ["WITH" ConditionalExpression] + * + * @return Doctrine\ORM\Query\AST\Join + */ + public function Join() + { + // Check Join type + $joinType = AST\Join::JOIN_TYPE_INNER; + + if ($this->_lexer->isNextToken(Lexer::T_LEFT)) { + $this->match(Lexer::T_LEFT); + + // Possible LEFT OUTER join + if ($this->_lexer->isNextToken(Lexer::T_OUTER)) { + $this->match(Lexer::T_OUTER); + $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; + } else { + $joinType = AST\Join::JOIN_TYPE_LEFT; + } + } else if ($this->_lexer->isNextToken(Lexer::T_INNER)) { + $this->match(Lexer::T_INNER); + } + + $this->match(Lexer::T_JOIN); + + $joinPathExpression = $this->JoinAssociationPathExpression(); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + $token = $this->_lexer->lookahead; + $aliasIdentificationVariable = $this->AliasIdentificationVariable(); + + // Verify that the association exists. + $parentClass = $this->_queryComponents[$joinPathExpression->identificationVariable]['metadata']; + $assocField = $joinPathExpression->associationField; + + if ( ! $parentClass->hasAssociation($assocField)) { + $this->semanticalError( + "Class " . $parentClass->name . " has no association named '$assocField'." + ); + } + + $targetClassName = $parentClass->associationMappings[$assocField]['targetEntity']; + + // Building queryComponent + $joinQueryComponent = array( + 'metadata' => $this->_em->getClassMetadata($targetClassName), + 'parent' => $joinPathExpression->identificationVariable, + 'relation' => $parentClass->getAssociationMapping($assocField), + 'map' => null, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token + ); + $this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; + + // Create AST node + $join = new AST\Join($joinType, $joinPathExpression, $aliasIdentificationVariable); + + // Check for ad-hoc Join conditions + if ($this->_lexer->isNextToken(Lexer::T_WITH)) { + $this->match(Lexer::T_WITH); + $join->conditionalExpression = $this->ConditionalExpression(); + } + + return $join; + } + + /** + * IndexBy ::= "INDEX" "BY" StateFieldPathExpression + * + * @return Doctrine\ORM\Query\AST\IndexBy + */ + public function IndexBy() + { + $this->match(Lexer::T_INDEX); + $this->match(Lexer::T_BY); + $pathExpr = $this->StateFieldPathExpression(); + + // Add the INDEX BY info to the query component + $this->_queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field; + + return new AST\IndexBy($pathExpr); + } + + /** + * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | + * StateFieldPathExpression | BooleanPrimary | CaseExpression | + * EntityTypeExpression + * + * @return mixed One of the possible expressions or subexpressions. + */ + public function ScalarExpression() + { + $lookahead = $this->_lexer->lookahead['type']; + if ($lookahead === Lexer::T_IDENTIFIER) { + $this->_lexer->peek(); // lookahead => '.' + $this->_lexer->peek(); // lookahead => token after '.' + $peek = $this->_lexer->peek(); // lookahead => token after the token after the '.' + $this->_lexer->resetPeek(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + return $this->StateFieldPathExpression(); + } else if ($lookahead == Lexer::T_INTEGER || $lookahead == Lexer::T_FLOAT) { + return $this->SimpleArithmeticExpression(); + } else if ($this->_isFunction()) { + // We may be in an ArithmeticExpression (find the matching ")" and inspect for Math operator) + $this->_lexer->peek(); // "(" + $peek = $this->_peekBeyondClosingParenthesis(); + + if ($this->_isMathOperator($peek)) { + return $this->SimpleArithmeticExpression(); + } + + return $this->FunctionDeclaration(); + } else if ($lookahead == Lexer::T_STRING) { + return $this->StringPrimary(); + } else if ($lookahead == Lexer::T_INPUT_PARAMETER) { + return $this->InputParameter(); + } else if ($lookahead == Lexer::T_TRUE || $lookahead == Lexer::T_FALSE) { + $this->match($lookahead); + return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); + } else if ($lookahead == Lexer::T_CASE || $lookahead == Lexer::T_COALESCE || $lookahead == Lexer::T_NULLIF) { + return $this->CaseExpression(); + } else { + $this->syntaxError(); + } + } + + public function CaseExpression() + { + // if "CASE" "WHEN" => GeneralCaseExpression + // else if "CASE" => SimpleCaseExpression + // else if "COALESCE" => CoalesceExpression + // else if "NULLIF" => NullifExpression + $this->semanticalError('CaseExpression not yet supported.'); + } + + /** + * SelectExpression ::= + * IdentificationVariable | StateFieldPathExpression | + * (AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable] + * + * @return Doctrine\ORM\Query\AST\SelectExpression + */ + public function SelectExpression() + { + $expression = null; + $identVariable = null; + $fieldAliasIdentificationVariable = null; + $peek = $this->_lexer->glimpse(); + + $supportsAlias = true; + + if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { + if ($peek['value'] == '.') { + // ScalarExpression + $expression = $this->ScalarExpression(); + } else { + $supportsAlias = false; + $expression = $identVariable = $this->IdentificationVariable(); + } + } else if ($this->_lexer->lookahead['value'] == '(') { + if ($peek['type'] == Lexer::T_SELECT) { + // Subselect + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); + } + } else if ($this->_isFunction()) { + $this->_lexer->peek(); // "(" + $beyond = $this->_peekBeyondClosingParenthesis(); + + if ($this->_isMathOperator($beyond)) { + $expression = $this->ScalarExpression(); + } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + $expression = $this->AggregateExpression(); + } else { + // Shortcut: ScalarExpression => Function + $expression = $this->FunctionDeclaration(); + } + } else if ($this->_lexer->lookahead['type'] == Lexer::T_PARTIAL) { + $supportsAlias = false; + $expression = $this->PartialObjectExpression(); + $identVariable = $expression->identificationVariable; + } else if ($this->_lexer->lookahead['type'] == Lexer::T_INTEGER || + $this->_lexer->lookahead['type'] == Lexer::T_FLOAT) { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); + } else { + $this->syntaxError('IdentificationVariable | StateFieldPathExpression' + . ' | AggregateExpression | "(" Subselect ")" | ScalarExpression', + $this->_lexer->lookahead); + } + + if ($supportsAlias) { + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $token = $this->_lexer->lookahead; + $fieldAliasIdentificationVariable = $this->AliasResultVariable(); + + // Include AliasResultVariable in query components. + $this->_queryComponents[$fieldAliasIdentificationVariable] = array( + 'resultVariable' => $expression, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + } + } + + $expr = new AST\SelectExpression($expression, $fieldAliasIdentificationVariable); + if (!$supportsAlias) { + $this->_identVariableExpressions[$identVariable] = $expr; + } + return $expr; + } + + /** + * SimpleSelectExpression ::= + * StateFieldPathExpression | IdentificationVariable | + * ((AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]) + * + * @return \Doctrine\ORM\Query\AST\SimpleSelectExpression + */ + public function SimpleSelectExpression() + { + $peek = $this->_lexer->glimpse(); + + if ($peek['value'] != '(' && $this->_lexer->lookahead['type'] === Lexer::T_IDENTIFIER) { + // SingleValuedPathExpression | IdentificationVariable + if ($peek['value'] == '.') { + $expression = $this->StateFieldPathExpression(); + } else { + $expression = $this->IdentificationVariable(); + } + + return new AST\SimpleSelectExpression($expression); + } else if ($this->_lexer->lookahead['value'] == '(') { + if ($peek['type'] == Lexer::T_SELECT) { + // Subselect + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expression = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else { + // Shortcut: ScalarExpression => SimpleArithmeticExpression + $expression = $this->SimpleArithmeticExpression(); + } + + return new AST\SimpleSelectExpression($expression); + } + + $this->_lexer->peek(); + $beyond = $this->_peekBeyondClosingParenthesis(); + + if ($this->_isMathOperator($beyond)) { + $expression = $this->ScalarExpression(); + } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + $expression = $this->AggregateExpression(); + } else { + $expression = $this->FunctionDeclaration(); + } + + $expr = new AST\SimpleSelectExpression($expression); + + if ($this->_lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::T_AS); + } + + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $token = $this->_lexer->lookahead; + $resultVariable = $this->AliasResultVariable(); + $expr->fieldIdentificationVariable = $resultVariable; + + // Include AliasResultVariable in query components. + $this->_queryComponents[$resultVariable] = array( + 'resultvariable' => $expr, + 'nestingLevel' => $this->_nestingLevel, + 'token' => $token, + ); + } + + return $expr; + } + + /** + * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* + * + * @return \Doctrine\ORM\Query\AST\ConditionalExpression + */ + public function ConditionalExpression() + { + $conditionalTerms = array(); + $conditionalTerms[] = $this->ConditionalTerm(); + + while ($this->_lexer->isNextToken(Lexer::T_OR)) { + $this->match(Lexer::T_OR); + $conditionalTerms[] = $this->ConditionalTerm(); + } + + // Phase 1 AST optimization: Prevent AST\ConditionalExpression + // if only one AST\ConditionalTerm is defined + if (count($conditionalTerms) == 1) { + return $conditionalTerms[0]; + } + + return new AST\ConditionalExpression($conditionalTerms); + } + + /** + * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* + * + * @return \Doctrine\ORM\Query\AST\ConditionalTerm + */ + public function ConditionalTerm() + { + $conditionalFactors = array(); + $conditionalFactors[] = $this->ConditionalFactor(); + + while ($this->_lexer->isNextToken(Lexer::T_AND)) { + $this->match(Lexer::T_AND); + $conditionalFactors[] = $this->ConditionalFactor(); + } + + // Phase 1 AST optimization: Prevent AST\ConditionalTerm + // if only one AST\ConditionalFactor is defined + if (count($conditionalFactors) == 1) { + return $conditionalFactors[0]; + } + + return new AST\ConditionalTerm($conditionalFactors); + } + + /** + * ConditionalFactor ::= ["NOT"] ConditionalPrimary + * + * @return \Doctrine\ORM\Query\AST\ConditionalFactor + */ + public function ConditionalFactor() + { + $not = false; + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $not = true; + } + + $conditionalPrimary = $this->ConditionalPrimary(); + + // Phase 1 AST optimization: Prevent AST\ConditionalFactor + // if only one AST\ConditionalPrimary is defined + if ( ! $not) { + return $conditionalPrimary; + } + + $conditionalFactor = new AST\ConditionalFactor($conditionalPrimary); + $conditionalFactor->not = $not; + + return $conditionalFactor; + } + + /** + * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" + * + * @return Doctrine\ORM\Query\AST\ConditionalPrimary + */ + public function ConditionalPrimary() + { + $condPrimary = new AST\ConditionalPrimary; + + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { + // Peek beyond the matching closing paranthesis ')' + $peek = $this->_peekBeyondClosingParenthesis(); + + if (in_array($peek['value'], array("=", "<", "<=", "<>", ">", ">=", "!=")) || + $peek['type'] === Lexer::T_NOT || + $peek['type'] === Lexer::T_BETWEEN || + $peek['type'] === Lexer::T_LIKE || + $peek['type'] === Lexer::T_IN || + $peek['type'] === Lexer::T_IS || + $peek['type'] === Lexer::T_EXISTS) { + $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); + } else { + $this->match(Lexer::T_OPEN_PARENTHESIS); + $condPrimary->conditionalExpression = $this->ConditionalExpression(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } + } else { + $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); + } + + return $condPrimary; + } + + /** + * SimpleConditionalExpression ::= + * ComparisonExpression | BetweenExpression | LikeExpression | + * InExpression | NullComparisonExpression | ExistsExpression | + * EmptyCollectionComparisonExpression | CollectionMemberExpression | + * InstanceOfExpression + */ + public function SimpleConditionalExpression() + { + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $token = $this->_lexer->glimpse(); + } else { + $token = $this->_lexer->lookahead; + } + + if ($token['type'] === Lexer::T_EXISTS) { + return $this->ExistsExpression(); + } + + $peek = $this->_lexer->glimpse(); + + if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER) { + if ($peek['value'] == '(') { + // Peek beyond the matching closing paranthesis ')' + $this->_lexer->peek(); + $token = $this->_peekBeyondClosingParenthesis(); + } else { + // Peek beyond the PathExpression (or InputParameter) + $peek = $this->_lexer->peek(); + + while ($peek['value'] === '.') { + $this->_lexer->peek(); + $peek = $this->_lexer->peek(); + } + + // Also peek beyond a NOT if there is one + if ($peek['type'] === Lexer::T_NOT) { + $peek = $this->_lexer->peek(); + } + + $token = $peek; + + // We need to go even further in case of IS (differenciate between NULL and EMPTY) + $lookahead = $this->_lexer->peek(); + + // Also peek beyond a NOT if there is one + if ($lookahead['type'] === Lexer::T_NOT) { + $lookahead = $this->_lexer->peek(); + } + + $this->_lexer->resetPeek(); + } + } + + switch ($token['type']) { + case Lexer::T_BETWEEN: + return $this->BetweenExpression(); + case Lexer::T_LIKE: + return $this->LikeExpression(); + case Lexer::T_IN: + return $this->InExpression(); + case Lexer::T_INSTANCE: + return $this->InstanceOfExpression(); + case Lexer::T_IS: + if ($lookahead['type'] == Lexer::T_NULL) { + return $this->NullComparisonExpression(); + } + return $this->EmptyCollectionComparisonExpression(); + case Lexer::T_MEMBER: + return $this->CollectionMemberExpression(); + default: + return $this->ComparisonExpression(); + } + } + + /** + * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" + * + * @return \Doctrine\ORM\Query\AST\EmptyCollectionComparisonExpression + */ + public function EmptyCollectionComparisonExpression() + { + $emptyColletionCompExpr = new AST\EmptyCollectionComparisonExpression( + $this->CollectionValuedPathExpression() + ); + $this->match(Lexer::T_IS); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $emptyColletionCompExpr->not = true; + } + + $this->match(Lexer::T_EMPTY); + + return $emptyColletionCompExpr; + } + + /** + * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression + * + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + * + * @return \Doctrine\ORM\Query\AST\CollectionMemberExpression + */ + public function CollectionMemberExpression() + { + $not = false; + + $entityExpr = $this->EntityExpression(); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $not = true; + $this->match(Lexer::T_NOT); + } + + $this->match(Lexer::T_MEMBER); + + if ($this->_lexer->isNextToken(Lexer::T_OF)) { + $this->match(Lexer::T_OF); + } + + $collMemberExpr = new AST\CollectionMemberExpression( + $entityExpr, $this->CollectionValuedPathExpression() + ); + $collMemberExpr->not = $not; + + return $collMemberExpr; + } + + /** + * Literal ::= string | char | integer | float | boolean + * + * @return string + */ + public function Literal() + { + switch ($this->_lexer->lookahead['type']) { + case Lexer::T_STRING: + $this->match(Lexer::T_STRING); + return new AST\Literal(AST\Literal::STRING, $this->_lexer->token['value']); + + case Lexer::T_INTEGER: + case Lexer::T_FLOAT: + $this->match( + $this->_lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT + ); + return new AST\Literal(AST\Literal::NUMERIC, $this->_lexer->token['value']); + + case Lexer::T_TRUE: + case Lexer::T_FALSE: + $this->match( + $this->_lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE + ); + return new AST\Literal(AST\Literal::BOOLEAN, $this->_lexer->token['value']); + + default: + $this->syntaxError('Literal'); + } + } + + /** + * InParameter ::= Literal | InputParameter + * + * @return string | \Doctrine\ORM\Query\AST\InputParameter + */ + public function InParameter() + { + if ($this->_lexer->lookahead['type'] == Lexer::T_INPUT_PARAMETER) { + return $this->InputParameter(); + } + + return $this->Literal(); + } + + /** + * InputParameter ::= PositionalParameter | NamedParameter + * + * @return \Doctrine\ORM\Query\AST\InputParameter + */ + public function InputParameter() + { + $this->match(Lexer::T_INPUT_PARAMETER); + + return new AST\InputParameter($this->_lexer->token['value']); + } + + /** + * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" + * + * @return \Doctrine\ORM\Query\AST\ArithmeticExpression + */ + public function ArithmeticExpression() + { + $expr = new AST\ArithmeticExpression; + + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { + $peek = $this->_lexer->glimpse(); + + if ($peek['type'] === Lexer::T_SELECT) { + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expr->subselect = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $expr; + } + } + + $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); + + return $expr; + } + + /** + * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* + * + * @return \Doctrine\ORM\Query\AST\SimpleArithmeticExpression + */ + public function SimpleArithmeticExpression() + { + $terms = array(); + $terms[] = $this->ArithmeticTerm(); + + while (($isPlus = $this->_lexer->isNextToken(Lexer::T_PLUS)) || $this->_lexer->isNextToken(Lexer::T_MINUS)) { + $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS); + + $terms[] = $this->_lexer->token['value']; + $terms[] = $this->ArithmeticTerm(); + } + + // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression + // if only one AST\ArithmeticTerm is defined + if (count($terms) == 1) { + return $terms[0]; + } + + return new AST\SimpleArithmeticExpression($terms); + } + + /** + * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* + * + * @return \Doctrine\ORM\Query\AST\ArithmeticTerm + */ + public function ArithmeticTerm() + { + $factors = array(); + $factors[] = $this->ArithmeticFactor(); + + while (($isMult = $this->_lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->_lexer->isNextToken(Lexer::T_DIVIDE)) { + $this->match(($isMult) ? Lexer::T_MULTIPLY : Lexer::T_DIVIDE); + + $factors[] = $this->_lexer->token['value']; + $factors[] = $this->ArithmeticFactor(); + } + + // Phase 1 AST optimization: Prevent AST\ArithmeticTerm + // if only one AST\ArithmeticFactor is defined + if (count($factors) == 1) { + return $factors[0]; + } + + return new AST\ArithmeticTerm($factors); + } + + /** + * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary + * + * @return \Doctrine\ORM\Query\AST\ArithmeticFactor + */ + public function ArithmeticFactor() + { + $sign = null; + + if (($isPlus = $this->_lexer->isNextToken(Lexer::T_PLUS)) || $this->_lexer->isNextToken(Lexer::T_MINUS)) { + $this->match(($isPlus) ? Lexer::T_PLUS : Lexer::T_MINUS); + $sign = $isPlus; + } + + $primary = $this->ArithmeticPrimary(); + + // Phase 1 AST optimization: Prevent AST\ArithmeticFactor + // if only one AST\ArithmeticPrimary is defined + if ($sign === null) { + return $primary; + } + + return new AST\ArithmeticFactor($primary, $sign); + } + + /** + * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | "(" SimpleArithmeticExpression ")" + * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings + * | FunctionsReturningDatetime | IdentificationVariable + */ + public function ArithmeticPrimary() + { + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expr = $this->SimpleArithmeticExpression(); + + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $expr; + } + + switch ($this->_lexer->lookahead['type']) { + case Lexer::T_IDENTIFIER: + $peek = $this->_lexer->glimpse(); + + if ($peek['value'] == '(') { + return $this->FunctionDeclaration(); + } + + if ($peek['value'] == '.') { + return $this->SingleValuedPathExpression(); + } + + return $this->StateFieldPathExpression(); + + case Lexer::T_INPUT_PARAMETER: + return $this->InputParameter(); + + default: + $peek = $this->_lexer->glimpse(); + + if ($peek['value'] == '(') { + if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } + + return $this->FunctionDeclaration(); + } else { + return $this->Literal(); + } + } + } + + /** + * StringExpression ::= StringPrimary | "(" Subselect ")" + * + * @return \Doctrine\ORM\Query\AST\StringPrimary | + * \Doctrine]ORM\Query\AST\Subselect + */ + public function StringExpression() + { + if ($this->_lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { + $peek = $this->_lexer->glimpse(); + + if ($peek['type'] === Lexer::T_SELECT) { + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expr = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $expr; + } + } + + return $this->StringPrimary(); + } + + /** + * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression + */ + public function StringPrimary() + { + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER)) { + $peek = $this->_lexer->glimpse(); + + if ($peek['value'] == '.') { + return $this->StateFieldPathExpression(); + } else if ($peek['value'] == '(') { + return $this->FunctionsReturningStrings(); + } else { + $this->syntaxError("'.' or '('"); + } + } else if ($this->_lexer->isNextToken(Lexer::T_STRING)) { + $this->match(Lexer::T_STRING); + + return $this->_lexer->token['value']; + } else if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + return $this->InputParameter(); + } else if ($this->_isAggregateFunction($this->_lexer->lookahead['type'])) { + return $this->AggregateExpression(); + } + + $this->syntaxError('StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'); + } + + /** + * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression + * + * @return \Doctrine\ORM\Query\AST\SingleValuedAssociationPathExpression | + * \Doctrine\ORM\Query\AST\SimpleEntityExpression + */ + public function EntityExpression() + { + $glimpse = $this->_lexer->glimpse(); + + if ($this->_lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') { + return $this->SingleValuedAssociationPathExpression(); + } + + return $this->SimpleEntityExpression(); + } + + /** + * SimpleEntityExpression ::= IdentificationVariable | InputParameter + * + * @return string | \Doctrine\ORM\Query\AST\InputParameter + */ + public function SimpleEntityExpression() + { + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + return $this->InputParameter(); + } + + return $this->IdentificationVariable(); + } + + /** + * AggregateExpression ::= + * ("AVG" | "MAX" | "MIN" | "SUM") "(" ["DISTINCT"] StateFieldPathExpression ")" | + * "COUNT" "(" ["DISTINCT"] (IdentificationVariable | SingleValuedPathExpression) ")" + * + * @return \Doctrine\ORM\Query\AST\AggregateExpression + */ + public function AggregateExpression() + { + $isDistinct = false; + $functionName = ''; + + if ($this->_lexer->isNextToken(Lexer::T_COUNT)) { + $this->match(Lexer::T_COUNT); + $functionName = $this->_lexer->token['value']; + $this->match(Lexer::T_OPEN_PARENTHESIS); + + if ($this->_lexer->isNextToken(Lexer::T_DISTINCT)) { + $this->match(Lexer::T_DISTINCT); + $isDistinct = true; + } + + $pathExp = $this->SingleValuedPathExpression(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } else { + if ($this->_lexer->isNextToken(Lexer::T_AVG)) { + $this->match(Lexer::T_AVG); + } else if ($this->_lexer->isNextToken(Lexer::T_MAX)) { + $this->match(Lexer::T_MAX); + } else if ($this->_lexer->isNextToken(Lexer::T_MIN)) { + $this->match(Lexer::T_MIN); + } else if ($this->_lexer->isNextToken(Lexer::T_SUM)) { + $this->match(Lexer::T_SUM); + } else { + $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); + } + + $functionName = $this->_lexer->token['value']; + $this->match(Lexer::T_OPEN_PARENTHESIS); + $pathExp = $this->StateFieldPathExpression(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); + } + + return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); + } + + /** + * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" + * + * @return \Doctrine\ORM\Query\AST\QuantifiedExpression + */ + public function QuantifiedExpression() + { + $type = ''; + + if ($this->_lexer->isNextToken(Lexer::T_ALL)) { + $this->match(Lexer::T_ALL); + $type = 'ALL'; + } else if ($this->_lexer->isNextToken(Lexer::T_ANY)) { + $this->match(Lexer::T_ANY); + $type = 'ANY'; + } else if ($this->_lexer->isNextToken(Lexer::T_SOME)) { + $this->match(Lexer::T_SOME); + $type = 'SOME'; + } else { + $this->syntaxError('ALL, ANY or SOME'); + } + + $this->match(Lexer::T_OPEN_PARENTHESIS); + $qExpr = new AST\QuantifiedExpression($this->Subselect()); + $qExpr->type = $type; + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $qExpr; + } + + /** + * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression + * + * @return \Doctrine\ORM\Query\AST\BetweenExpression + */ + public function BetweenExpression() + { + $not = false; + $arithExpr1 = $this->ArithmeticExpression(); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $not = true; + } + + $this->match(Lexer::T_BETWEEN); + $arithExpr2 = $this->ArithmeticExpression(); + $this->match(Lexer::T_AND); + $arithExpr3 = $this->ArithmeticExpression(); + + $betweenExpr = new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3); + $betweenExpr->not = $not; + + return $betweenExpr; + } + + /** + * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) + * + * @return \Doctrine\ORM\Query\AST\ComparisonExpression + */ + public function ComparisonExpression() + { + $peek = $this->_lexer->glimpse(); + + $leftExpr = $this->ArithmeticExpression(); + $operator = $this->ComparisonOperator(); + + if ($this->_isNextAllAnySome()) { + $rightExpr = $this->QuantifiedExpression(); + } else { + $rightExpr = $this->ArithmeticExpression(); + } + + return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); + } + + /** + * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")" + * + * @return \Doctrine\ORM\Query\AST\InExpression + */ + public function InExpression() + { + $inExpression = new AST\InExpression($this->SingleValuedPathExpression()); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $inExpression->not = true; + } + + $this->match(Lexer::T_IN); + $this->match(Lexer::T_OPEN_PARENTHESIS); + + if ($this->_lexer->isNextToken(Lexer::T_SELECT)) { + $inExpression->subselect = $this->Subselect(); + } else { + $literals = array(); + $literals[] = $this->InParameter(); + + while ($this->_lexer->isNextToken(Lexer::T_COMMA)) { + $this->match(Lexer::T_COMMA); + $literals[] = $this->InParameter(); + } + + $inExpression->literals = $literals; + } + + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $inExpression; + } + + /** + * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (AbstractSchemaName | InputParameter) + * + * @return \Doctrine\ORM\Query\AST\InstanceOfExpression + */ + public function InstanceOfExpression() + { + $instanceOfExpression = new AST\InstanceOfExpression($this->IdentificationVariable()); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $instanceOfExpression->not = true; + } + + $this->match(Lexer::T_INSTANCE); + + if ($this->_lexer->isNextToken(Lexer::T_OF)) { + $this->match(Lexer::T_OF); + } + + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + $this->match(Lexer::T_INPUT_PARAMETER); + $exprValue = new AST\InputParameter($this->_lexer->token['value']); + } else { + $exprValue = $this->AliasIdentificationVariable(); + } + + $instanceOfExpression->value = $exprValue; + + return $instanceOfExpression; + } + + /** + * LikeExpression ::= StringExpression ["NOT"] "LIKE" (string | input_parameter) ["ESCAPE" char] + * + * @return \Doctrine\ORM\Query\AST\LikeExpression + */ + public function LikeExpression() + { + $stringExpr = $this->StringExpression(); + $not = false; + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $not = true; + } + + $this->match(Lexer::T_LIKE); + + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + $this->match(Lexer::T_INPUT_PARAMETER); + $stringPattern = new AST\InputParameter($this->_lexer->token['value']); + } else { + $this->match(Lexer::T_STRING); + $stringPattern = $this->_lexer->token['value']; + } + + $escapeChar = null; + + if ($this->_lexer->lookahead['type'] === Lexer::T_ESCAPE) { + $this->match(Lexer::T_ESCAPE); + $this->match(Lexer::T_STRING); + $escapeChar = $this->_lexer->token['value']; + } + + $likeExpr = new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar); + $likeExpr->not = $not; + + return $likeExpr; + } + + /** + * NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" + * + * @return \Doctrine\ORM\Query\AST\NullComparisonExpression + */ + public function NullComparisonExpression() + { + if ($this->_lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { + $this->match(Lexer::T_INPUT_PARAMETER); + $expr = new AST\InputParameter($this->_lexer->token['value']); + } else { + $expr = $this->SingleValuedPathExpression(); + } + + $nullCompExpr = new AST\NullComparisonExpression($expr); + $this->match(Lexer::T_IS); + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $nullCompExpr->not = true; + } + + $this->match(Lexer::T_NULL); + + return $nullCompExpr; + } + + /** + * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" + * + * @return \Doctrine\ORM\Query\AST\ExistsExpression + */ + public function ExistsExpression() + { + $not = false; + + if ($this->_lexer->isNextToken(Lexer::T_NOT)) { + $this->match(Lexer::T_NOT); + $not = true; + } + + $this->match(Lexer::T_EXISTS); + $this->match(Lexer::T_OPEN_PARENTHESIS); + $existsExpression = new AST\ExistsExpression($this->Subselect()); + $existsExpression->not = $not; + $this->match(Lexer::T_CLOSE_PARENTHESIS); + + return $existsExpression; + } + + /** + * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" + * + * @return string + */ + public function ComparisonOperator() + { + switch ($this->_lexer->lookahead['value']) { + case '=': + $this->match(Lexer::T_EQUALS); + + return '='; + + case '<': + $this->match(Lexer::T_LOWER_THAN); + $operator = '<'; + + if ($this->_lexer->isNextToken(Lexer::T_EQUALS)) { + $this->match(Lexer::T_EQUALS); + $operator .= '='; + } else if ($this->_lexer->isNextToken(Lexer::T_GREATER_THAN)) { + $this->match(Lexer::T_GREATER_THAN); + $operator .= '>'; + } + + return $operator; + + case '>': + $this->match(Lexer::T_GREATER_THAN); + $operator = '>'; + + if ($this->_lexer->isNextToken(Lexer::T_EQUALS)) { + $this->match(Lexer::T_EQUALS); + $operator .= '='; + } + + return $operator; + + case '!': + $this->match(Lexer::T_NEGATE); + $this->match(Lexer::T_EQUALS); + + return '<>'; + + default: + $this->syntaxError('=, <, <=, <>, >, >=, !='); + } + } + + /** + * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime + */ + public function FunctionDeclaration() + { + $token = $this->_lexer->lookahead; + $funcName = strtolower($token['value']); + + // Check for built-in functions first! + if (isset(self::$_STRING_FUNCTIONS[$funcName])) { + return $this->FunctionsReturningStrings(); + } else if (isset(self::$_NUMERIC_FUNCTIONS[$funcName])) { + return $this->FunctionsReturningNumerics(); + } else if (isset(self::$_DATETIME_FUNCTIONS[$funcName])) { + return $this->FunctionsReturningDatetime(); + } + + // Check for custom functions afterwards + $config = $this->_em->getConfiguration(); + + if ($config->getCustomStringFunction($funcName) !== null) { + return $this->CustomFunctionsReturningStrings(); + } else if ($config->getCustomNumericFunction($funcName) !== null) { + return $this->CustomFunctionsReturningNumerics(); + } else if ($config->getCustomDatetimeFunction($funcName) !== null) { + return $this->CustomFunctionsReturningDatetime(); + } + + $this->syntaxError('known function', $token); + } + + /** + * FunctionsReturningNumerics ::= + * "LENGTH" "(" StringPrimary ")" | + * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | + * "ABS" "(" SimpleArithmeticExpression ")" | + * "SQRT" "(" SimpleArithmeticExpression ")" | + * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | + * "SIZE" "(" CollectionValuedPathExpression ")" + */ + public function FunctionsReturningNumerics() + { + $funcNameLower = strtolower($this->_lexer->lookahead['value']); + $funcClass = self::$_NUMERIC_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningNumerics() + { + $funcName = strtolower($this->_lexer->lookahead['value']); + // getCustomNumericFunction is case-insensitive + $funcClass = $this->_em->getConfiguration()->getCustomNumericFunction($funcName); + $function = new $funcClass($funcName); + $function->parse($this); + + return $function; + } + + /** + * FunctionsReturningDateTime ::= "CURRENT_DATE" | "CURRENT_TIME" | "CURRENT_TIMESTAMP" + */ + public function FunctionsReturningDatetime() + { + $funcNameLower = strtolower($this->_lexer->lookahead['value']); + $funcClass = self::$_DATETIME_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningDatetime() + { + $funcName = $this->_lexer->lookahead['value']; + // getCustomDatetimeFunction is case-insensitive + $funcClass = $this->_em->getConfiguration()->getCustomDatetimeFunction($funcName); + $function = new $funcClass($funcName); + $function->parse($this); + + return $function; + } + + /** + * FunctionsReturningStrings ::= + * "CONCAT" "(" StringPrimary "," StringPrimary ")" | + * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | + * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | + * "LOWER" "(" StringPrimary ")" | + * "UPPER" "(" StringPrimary ")" + */ + public function FunctionsReturningStrings() + { + $funcNameLower = strtolower($this->_lexer->lookahead['value']); + $funcClass = self::$_STRING_FUNCTIONS[$funcNameLower]; + $function = new $funcClass($funcNameLower); + $function->parse($this); + + return $function; + } + + public function CustomFunctionsReturningStrings() + { + $funcName = $this->_lexer->lookahead['value']; + // getCustomStringFunction is case-insensitive + $funcClass = $this->_em->getConfiguration()->getCustomStringFunction($funcName); + $function = new $funcClass($funcName); + $function->parse($this); + + return $function; + } +} diff --git a/Doctrine/ORM/Query/ParserResult.php b/Doctrine/ORM/Query/ParserResult.php new file mode 100644 index 0000000..42aecc1 --- /dev/null +++ b/Doctrine/ORM/Query/ParserResult.php @@ -0,0 +1,141 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * Encapsulates the resulting components from a DQL query parsing process that + * can be serialized. + * + * @author Guilherme Blanco + * @author Janne Vanhala + * @author Roman Borschel + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + */ +class ParserResult +{ + /** + * The SQL executor used for executing the SQL. + * + * @var \Doctrine\ORM\Query\Exec\AbstractSqlExecutor + */ + private $_sqlExecutor; + + /** + * The ResultSetMapping that describes how to map the SQL result set. + * + * @var \Doctrine\ORM\Query\ResultSetMapping + */ + private $_resultSetMapping; + + /** + * The mappings of DQL parameter names/positions to SQL parameter positions. + * + * @var array + */ + private $_parameterMappings = array(); + + /** + * Initializes a new instance of the ParserResult class. + * The new instance is initialized with an empty ResultSetMapping. + */ + public function __construct() + { + $this->_resultSetMapping = new ResultSetMapping; + } + + /** + * Gets the ResultSetMapping for the parsed query. + * + * @return ResultSetMapping The result set mapping of the parsed query or NULL + * if the query is not a SELECT query. + */ + public function getResultSetMapping() + { + return $this->_resultSetMapping; + } + + /** + * Sets the ResultSetMapping of the parsed query. + * + * @param ResultSetMapping $rsm + */ + public function setResultSetMapping(ResultSetMapping $rsm) + { + $this->_resultSetMapping = $rsm; + } + + /** + * Sets the SQL executor that should be used for this ParserResult. + * + * @param \Doctrine\ORM\Query\Exec\AbstractSqlExecutor $executor + */ + public function setSqlExecutor($executor) + { + $this->_sqlExecutor = $executor; + } + + /** + * Gets the SQL executor used by this ParserResult. + * + * @return \Doctrine\ORM\Query\Exec\AbstractSqlExecutor + */ + public function getSqlExecutor() + { + return $this->_sqlExecutor; + } + + /** + * Adds a DQL to SQL parameter mapping. One DQL parameter name/position can map to + * several SQL parameter positions. + * + * @param string|integer $dqlPosition + * @param integer $sqlPosition + */ + public function addParameterMapping($dqlPosition, $sqlPosition) + { + $this->_parameterMappings[$dqlPosition][] = $sqlPosition; + } + + /** + * Gets all DQL to SQL parameter mappings. + * + * @return array The parameter mappings. + */ + public function getParameterMappings() + { + return $this->_parameterMappings; + } + + /** + * Gets the SQL parameter positions for a DQL parameter name/position. + * + * @param string|integer $dqlPosition The name or position of the DQL parameter. + * @return array The positions of the corresponding SQL parameters. + */ + public function getSqlParameterPositions($dqlPosition) + { + return $this->_parameterMappings[$dqlPosition]; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/Printer.php b/Doctrine/ORM/Query/Printer.php new file mode 100644 index 0000000..972ad51 --- /dev/null +++ b/Doctrine/ORM/Query/Printer.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * A parse tree printer for Doctrine Query Language parser. + * + * @author Janne Vanhala + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link http://www.phpdoctrine.org + * @since 2.0 + * @version $Revision$ + */ +class Printer +{ + /** + * Current indentation level + * + * @var int + */ + protected $_indent = 0; + + /** + * Defines whether parse tree is printed (default, false) or not (true). + * + * @var bool + */ + protected $_silent; + + /** + * Constructs a new parse tree printer. + * + * @param bool $silent Parse tree will not be printed if true. + */ + public function __construct($silent = false) + { + $this->_silent = $silent; + } + + /** + * Prints an opening parenthesis followed by production name and increases + * indentation level by one. + * + * This method is called before executing a production. + * + * @param string $name production name + */ + public function startProduction($name) + { + $this->println('(' . $name); + $this->_indent++; + } + + /** + * Decreases indentation level by one and prints a closing parenthesis. + * + * This method is called after executing a production. + */ + public function endProduction() + { + $this->_indent--; + $this->println(')'); + } + + /** + * Prints text indented with spaces depending on current indentation level. + * + * @param string $str text + */ + public function println($str) + { + if ( ! $this->_silent) { + echo str_repeat(' ', $this->_indent), $str, "\n"; + } + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/QueryException.php b/Doctrine/ORM/Query/QueryException.php new file mode 100644 index 0000000..f9dfd08 --- /dev/null +++ b/Doctrine/ORM/Query/QueryException.php @@ -0,0 +1,139 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\ORM\Query\AST\PathExpression; + +/** + * Description of QueryException + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision: 3938 $ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class QueryException extends \Doctrine\ORM\ORMException +{ + public static function syntaxError($message) + { + return new self('[Syntax Error] ' . $message); + } + + public static function semanticalError($message) + { + return new self('[Semantical Error] ' . $message); + } + + public static function invalidParameterType($expected, $received) + { + return new self('Invalid parameter type, ' . $received . ' given, but ' . $expected . ' expected.'); + } + + public static function invalidParameterPosition($pos) + { + return new self('Invalid parameter position: ' . $pos); + } + + public static function invalidParameterNumber() + { + return new self("Invalid parameter number: number of bound variables does not match number of tokens"); + } + + public static function invalidParameterFormat($value) + { + return new self('Invalid parameter format, '.$value.' given, but : or ? expected.'); + } + + public static function unknownParameter($key) + { + return new self("Invalid parameter: token ".$key." is not defined in the query."); + } + + public static function invalidPathExpression($pathExpr) + { + return new self( + "Invalid PathExpression '" . $pathExpr->identificationVariable . + "." . implode('.', $pathExpr->parts) . "'." + ); + } + + public static function invalidLiteral($literal) { + return new self("Invalid literal '$literal'"); + } + + /** + * @param Doctrine\ORM\Mapping\AssociationMapping $assoc + */ + public static function iterateWithFetchJoinCollectionNotAllowed($assoc) + { + return new self( + "Invalid query operation: Not allowed to iterate over fetch join collections ". + "in class ".$assoc['sourceEntity']." assocation ".$assoc['fieldName'] + ); + } + + public static function partialObjectsAreDangerous() + { + return new self( + "Loading partial objects is dangerous. Fetch full objects or consider " . + "using a different fetch mode. If you really want partial objects, " . + "set the doctrine.forcePartialLoad query hint to TRUE." + ); + } + + public static function overwritingJoinConditionsNotYetSupported($assoc) + { + return new self( + "Unsupported query operation: It is not yet possible to overwrite the join ". + "conditions in class ".$assoc['sourceEntityName']." assocation ".$assoc['fieldName'].". ". + "Use WITH to append additional join conditions to the association." + ); + } + + public static function associationPathInverseSideNotSupported() + { + return new self( + "A single-valued association path expression to an inverse side is not supported". + " in DQL queries. Use an explicit join instead." + ); + } + + public static function iterateWithFetchJoinNotAllowed($assoc) { + return new self( + "Iterate with fetch join in class " . $assoc['sourceEntity'] . + " using association " . $assoc['fieldName'] . " not allowed." + ); + } + + public static function associationPathCompositeKeyNotSupported() + { + return new self( + "A single-valued association path expression to an entity with a composite primary ". + "key is not supported. Explicitly name the components of the composite primary key ". + "in the query." + ); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/ResultSetMapping.php b/Doctrine/ORM/Query/ResultSetMapping.php new file mode 100644 index 0000000..7066405 --- /dev/null +++ b/Doctrine/ORM/Query/ResultSetMapping.php @@ -0,0 +1,396 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * A ResultSetMapping describes how a result set of an SQL query maps to a Doctrine result. + * + * IMPORTANT NOTE: + * The properties of this class are only public for fast internal READ access and to (drastically) + * reduce the size of serialized instances for more effective caching due to better (un-)serialization + * performance. + * + * Users should use the public methods. + * + * @author Roman Borschel + * @since 2.0 + * @todo Think about whether the number of lookup maps can be reduced. + */ +class ResultSetMapping +{ + /** + * Whether the result is mixed (contains scalar values together with field values). + * + * @ignore + * @var boolean + */ + public $isMixed = false; + /** + * Maps alias names to class names. + * + * @ignore + * @var array + */ + public $aliasMap = array(); + /** + * Maps alias names to related association field names. + * + * @ignore + * @var array + */ + public $relationMap = array(); + /** + * Maps alias names to parent alias names. + * + * @ignore + * @var array + */ + public $parentAliasMap = array(); + /** + * Maps column names in the result set to field names for each class. + * + * @ignore + * @var array + */ + public $fieldMappings = array(); + /** + * Maps column names in the result set to the alias/field name to use in the mapped result. + * + * @ignore + * @var array + */ + public $scalarMappings = array(); + /** + * Maps column names of meta columns (foreign keys, discriminator columns, ...) to field names. + * + * @ignore + * @var array + */ + public $metaMappings = array(); + /** + * Maps column names in the result set to the alias they belong to. + * + * @ignore + * @var array + */ + public $columnOwnerMap = array(); + /** + * List of columns in the result set that are used as discriminator columns. + * + * @ignore + * @var array + */ + public $discriminatorColumns = array(); + /** + * Maps alias names to field names that should be used for indexing. + * + * @ignore + * @var array + */ + public $indexByMap = array(); + /** + * Map from column names to class names that declare the field the column is mapped to. + * + * @ignore + * @var array + */ + public $declaringClasses = array(); + + /** + * Adds an entity result to this ResultSetMapping. + * + * @param string $class The class name of the entity. + * @param string $alias The alias for the class. The alias must be unique among all entity + * results or joined entity results within this ResultSetMapping. + * @todo Rename: addRootEntity + */ + public function addEntityResult($class, $alias) + { + $this->aliasMap[$alias] = $class; + } + + /** + * Sets a discriminator column for an entity result or joined entity result. + * The discriminator column will be used to determine the concrete class name to + * instantiate. + * + * @param string $alias The alias of the entity result or joined entity result the discriminator + * column should be used for. + * @param string $discrColumn The name of the discriminator column in the SQL result set. + * @todo Rename: addDiscriminatorColumn + */ + public function setDiscriminatorColumn($alias, $discrColumn) + { + $this->discriminatorColumns[$alias] = $discrColumn; + $this->columnOwnerMap[$discrColumn] = $alias; + } + + /** + * Sets a field to use for indexing an entity result or joined entity result. + * + * @param string $alias The alias of an entity result or joined entity result. + * @param string $fieldName The name of the field to use for indexing. + */ + public function addIndexBy($alias, $fieldName) + { + $this->indexByMap[$alias] = $fieldName; + } + + /** + * Checks whether an entity result or joined entity result with a given alias has + * a field set for indexing. + * + * @param string $alias + * @return boolean + * @todo Rename: isIndexed($alias) + */ + public function hasIndexBy($alias) + { + return isset($this->indexByMap[$alias]); + } + + /** + * Checks whether the column with the given name is mapped as a field result + * as part of an entity result or joined entity result. + * + * @param string $columnName The name of the column in the SQL result set. + * @return boolean + * @todo Rename: isField + */ + public function isFieldResult($columnName) + { + return isset($this->fieldMappings[$columnName]); + } + + /** + * Adds a field to the result that belongs to an entity or joined entity. + * + * @param string $alias The alias of the root entity or joined entity to which the field belongs. + * @param string $columnName The name of the column in the SQL result set. + * @param string $fieldName The name of the field on the declaring class. + * @param string $declaringClass The name of the class that declares/owns the specified field. + * When $alias refers to a superclass in a mapped hierarchy but + * the field $fieldName is defined on a subclass, specify that here. + * If not specified, the field is assumed to belong to the class + * designated by $alias. + * @todo Rename: addField + */ + public function addFieldResult($alias, $columnName, $fieldName, $declaringClass = null) + { + // column name (in result set) => field name + $this->fieldMappings[$columnName] = $fieldName; + // column name => alias of owner + $this->columnOwnerMap[$columnName] = $alias; + // field name => class name of declaring class + $this->declaringClasses[$columnName] = $declaringClass ?: $this->aliasMap[$alias]; + if ( ! $this->isMixed && $this->scalarMappings) { + $this->isMixed = true; + } + } + + /** + * Adds a joined entity result. + * + * @param string $class The class name of the joined entity. + * @param string $alias The unique alias to use for the joined entity. + * @param string $parentAlias The alias of the entity result that is the parent of this joined result. + * @param object $relation The association field that connects the parent entity result with the joined entity result. + * @todo Rename: addJoinedEntity + */ + public function addJoinedEntityResult($class, $alias, $parentAlias, $relation) + { + $this->aliasMap[$alias] = $class; + $this->parentAliasMap[$alias] = $parentAlias; + $this->relationMap[$alias] = $relation; + } + + /** + * Adds a scalar result mapping. + * + * @param string $columnName The name of the column in the SQL result set. + * @param string $alias The result alias with which the scalar result should be placed in the result structure. + * @todo Rename: addScalar + */ + public function addScalarResult($columnName, $alias) + { + $this->scalarMappings[$columnName] = $alias; + if ( ! $this->isMixed && $this->fieldMappings) { + $this->isMixed = true; + } + } + + /** + * Checks whether a column with a given name is mapped as a scalar result. + * + * @param string $columName The name of the column in the SQL result set. + * @return boolean + * @todo Rename: isScalar + */ + public function isScalarResult($columnName) + { + return isset($this->scalarMappings[$columnName]); + } + + /** + * Gets the name of the class of an entity result or joined entity result, + * identified by the given unique alias. + * + * @param string $alias + * @return string + */ + public function getClassName($alias) + { + return $this->aliasMap[$alias]; + } + + /** + * Gets the field alias for a column that is mapped as a scalar value. + * + * @param string $columnName The name of the column in the SQL result set. + * @return string + */ + public function getScalarAlias($columnName) + { + return $this->scalarMappings[$columnName]; + } + + /** + * Gets the name of the class that owns a field mapping for the specified column. + * + * @param string $columnName + * @return string + */ + public function getDeclaringClass($columnName) + { + return $this->declaringClasses[$columnName]; + } + + /** + * + * @param string $alias + * @return AssociationMapping + */ + public function getRelation($alias) + { + return $this->relationMap[$alias]; + } + + /** + * + * @param string $alias + * @return boolean + */ + public function isRelation($alias) + { + return isset($this->relationMap[$alias]); + } + + /** + * Gets the alias of the class that owns a field mapping for the specified column. + * + * @param string $columnName + * @return string + */ + public function getEntityAlias($columnName) + { + return $this->columnOwnerMap[$columnName]; + } + + /** + * Gets the parent alias of the given alias. + * + * @param string $alias + * @return string + */ + public function getParentAlias($alias) + { + return $this->parentAliasMap[$alias]; + } + + /** + * Checks whether the given alias has a parent alias. + * + * @param string $alias + * @return boolean + */ + public function hasParentAlias($alias) + { + return isset($this->parentAliasMap[$alias]); + } + + /** + * Gets the field name for a column name. + * + * @param string $columnName + * @return string + */ + public function getFieldName($columnName) + { + return $this->fieldMappings[$columnName]; + } + + /** + * + * @return array + */ + public function getAliasMap() + { + return $this->aliasMap; + } + + /** + * Gets the number of different entities that appear in the mapped result. + * + * @return integer + */ + public function getEntityResultCount() + { + return count($this->aliasMap); + } + + /** + * Checks whether this ResultSetMapping defines a mixed result. + * Mixed results can only occur in object and array (graph) hydration. In such a + * case a mixed result means that scalar values are mixed with objects/array in + * the result. + * + * @return boolean + */ + public function isMixedResult() + { + return $this->isMixed; + } + + /** + * Adds a meta column (foreign key or discriminator column) to the result set. + * + * @param $alias + * @param $columnName + * @param $fieldName + */ + public function addMetaResult($alias, $columnName, $fieldName) + { + $this->metaMappings[$columnName] = $fieldName; + $this->columnOwnerMap[$columnName] = $alias; + } +} + diff --git a/Doctrine/ORM/Query/SqlWalker.php b/Doctrine/ORM/Query/SqlWalker.php new file mode 100644 index 0000000..31f75f0 --- /dev/null +++ b/Doctrine/ORM/Query/SqlWalker.php @@ -0,0 +1,1835 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\DBAL\LockMode, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\Query, + Doctrine\ORM\Query\QueryException; + +/** + * The SqlWalker is a TreeWalker that walks over a DQL AST and constructs + * the corresponding SQL. + * + * @author Roman Borschel + * @author Benjamin Eberlei + * @since 2.0 + * @todo Rename: SQLWalker + */ +class SqlWalker implements TreeWalker +{ + /** + * @var ResultSetMapping + */ + private $_rsm; + + /** Counters for generating unique column aliases, table aliases and parameter indexes. */ + private $_aliasCounter = 0; + private $_tableAliasCounter = 0; + private $_scalarResultCounter = 1; + private $_sqlParamIndex = 1; + + /** + * @var ParserResult + */ + private $_parserResult; + + /** + * @var EntityManager + */ + private $_em; + + /** + * @var Doctrine\DBAL\Connection + */ + private $_conn; + + /** + * @var AbstractQuery + */ + private $_query; + + private $_tableAliasMap = array(); + + /** Map from result variable names to their SQL column alias names. */ + private $_scalarResultAliasMap = array(); + + /** Map of all components/classes that appear in the DQL query. */ + private $_queryComponents; + + /** A list of classes that appear in non-scalar SelectExpressions. */ + private $_selectedClasses = array(); + + /** + * The DQL alias of the root class of the currently traversed query. + */ + private $_rootAliases = array(); + + /** + * Flag that indicates whether to generate SQL table aliases in the SQL. + * These should only be generated for SELECT queries, not for UPDATE/DELETE. + */ + private $_useSqlTableAliases = true; + + /** + * The database platform abstraction. + * + * @var AbstractPlatform + */ + private $_platform; + + /** + * {@inheritDoc} + */ + public function __construct($query, $parserResult, array $queryComponents) + { + $this->_query = $query; + $this->_parserResult = $parserResult; + $this->_queryComponents = $queryComponents; + $this->_rsm = $parserResult->getResultSetMapping(); + $this->_em = $query->getEntityManager(); + $this->_conn = $this->_em->getConnection(); + $this->_platform = $this->_conn->getDatabasePlatform(); + } + + /** + * Gets the Query instance used by the walker. + * + * @return Query. + */ + public function getQuery() + { + return $this->_query; + } + + /** + * Gets the Connection used by the walker. + * + * @return Connection + */ + public function getConnection() + { + return $this->_conn; + } + + /** + * Gets the EntityManager used by the walker. + * + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /** + * Gets the information about a single query component. + * + * @param string $dqlAlias The DQL alias. + * @return array + */ + public function getQueryComponent($dqlAlias) + { + return $this->_queryComponents[$dqlAlias]; + } + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @return AbstractExecutor + */ + public function getExecutor($AST) + { + $isDeleteStatement = $AST instanceof AST\DeleteStatement; + $isUpdateStatement = $AST instanceof AST\UpdateStatement; + + if ($isDeleteStatement) { + $primaryClass = $this->_em->getClassMetadata( + $AST->deleteClause->abstractSchemaName + ); + + if ($primaryClass->isInheritanceTypeJoined()) { + return new Exec\MultiTableDeleteExecutor($AST, $this); + } else { + return new Exec\SingleTableDeleteUpdateExecutor($AST, $this); + } + } else if ($isUpdateStatement) { + $primaryClass = $this->_em->getClassMetadata( + $AST->updateClause->abstractSchemaName + ); + + if ($primaryClass->isInheritanceTypeJoined()) { + return new Exec\MultiTableUpdateExecutor($AST, $this); + } else { + return new Exec\SingleTableDeleteUpdateExecutor($AST, $this); + } + } + + return new Exec\SingleSelectExecutor($AST, $this); + } + + /** + * Generates a unique, short SQL table alias. + * + * @param string $tableName Table name + * @param string $dqlAlias The DQL alias. + * @return string Generated table alias. + */ + public function getSqlTableAlias($tableName, $dqlAlias = '') + { + $tableName .= $dqlAlias; + + if ( ! isset($this->_tableAliasMap[$tableName])) { + $this->_tableAliasMap[$tableName] = strtolower(substr($tableName, 0, 1)) . $this->_tableAliasCounter++ . '_'; + } + + return $this->_tableAliasMap[$tableName]; + } + + /** + * Forces the SqlWalker to use a specific alias for a table name, rather than + * generating an alias on its own. + * + * @param string $tableName + * @param string $alias + */ + public function setSqlTableAlias($tableName, $alias) + { + $this->_tableAliasMap[$tableName] = $alias; + + return $alias; + } + + /** + * Gets an SQL column alias for a column name. + * + * @param string $columnName + * @return string + */ + public function getSqlColumnAlias($columnName) + { + return $columnName . $this->_aliasCounter++; + } + + /** + * Generates the SQL JOINs that are necessary for Class Table Inheritance + * for the given class. + * + * @param ClassMetadata $class The class for which to generate the joins. + * @param string $dqlAlias The DQL alias of the class. + * @return string The SQL. + */ + private function _generateClassTableInheritanceJoins($class, $dqlAlias) + { + $sql = ''; + + $baseTableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + + // INNER JOIN parent class tables + foreach ($class->parentClasses as $parentClassName) { + $parentClass = $this->_em->getClassMetadata($parentClassName); + $tableAlias = $this->getSQLTableAlias($parentClass->table['name'], $dqlAlias); + // If this is a joined association we must use left joins to preserve the correct result. + $sql .= isset($this->_queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; + $sql .= 'JOIN ' . $parentClass->getQuotedTableName($this->_platform) + . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($class->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + + $columnName = $class->getQuotedColumnName($idField, $this->_platform); + $sql .= $baseTableAlias . '.' . $columnName + . ' = ' + . $tableAlias . '.' . $columnName; + } + } + + // LEFT JOIN subclass tables, if partial objects disallowed. + if ( ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClass->table['name'], $dqlAlias); + $sql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) + . ' ' . $tableAlias . ' ON '; + $first = true; + foreach ($class->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + + $columnName = $class->getQuotedColumnName($idField, $this->_platform); + $sql .= $baseTableAlias . '.' . $columnName + . ' = ' + . $tableAlias . '.' . $columnName; + } + } + } + + return $sql; + } + + private function _generateOrderedCollectionOrderByItems() + { + $sql = ''; + foreach ($this->_selectedClasses AS $dqlAlias => $class) { + $qComp = $this->_queryComponents[$dqlAlias]; + if (isset($qComp['relation']['orderBy'])) { + foreach ($qComp['relation']['orderBy'] AS $fieldName => $orientation) { + if ($qComp['metadata']->isInheritanceTypeJoined()) { + $tableName = $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName); + } else { + $tableName = $qComp['metadata']->table['name']; + } + + if ($sql != '') { + $sql .= ', '; + } + $sql .= $this->getSqlTableAlias($tableName, $dqlAlias) . '.' . + $qComp['metadata']->getQuotedColumnName($fieldName, $this->_platform) . " $orientation"; + } + } + } + return $sql; + } + + /** + * Generates a discriminator column SQL condition for the class with the given DQL alias. + * + * @param array $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. + * @return string + */ + private function _generateDiscriminatorColumnConditionSQL(array $dqlAliases) + { + $encapsulate = false; + $sql = ''; + + foreach ($dqlAliases as $dqlAlias) { + $class = $this->_queryComponents[$dqlAlias]['metadata']; + + if ($class->isInheritanceTypeSingleTable()) { + $conn = $this->_em->getConnection(); + $values = array(); + if ($class->discriminatorValue !== null) { // discrimnators can be 0 + $values[] = $conn->quote($class->discriminatorValue); + } + + foreach ($class->subClasses as $subclassName) { + $values[] = $conn->quote($this->_em->getClassMetadata($subclassName)->discriminatorValue); + } + + if ($sql != '') { + $sql .= ' AND '; + $encapsulate = true; + } + + $sql .= ($sql != '' ? ' AND ' : '') + . (($this->_useSqlTableAliases) ? $this->getSqlTableAlias($class->table['name'], $dqlAlias) . '.' : '') + . $class->discriminatorColumn['name'] . ' IN (' . implode(', ', $values) . ')'; + } + } + + return ($encapsulate) ? '(' . $sql . ')' : $sql; + } + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectStatement(AST\SelectStatement $AST) + { + $sql = $this->walkSelectClause($AST->selectClause); + $sql .= $this->walkFromClause($AST->fromClause); + + if (($whereClause = $AST->whereClause) !== null) { + $sql .= $this->walkWhereClause($whereClause); + } else if (($discSql = $this->_generateDiscriminatorColumnConditionSQL($this->_rootAliases)) !== '') { + $sql .= ' WHERE ' . $discSql; + } + + $sql .= $AST->groupByClause ? $this->walkGroupByClause($AST->groupByClause) : ''; + $sql .= $AST->havingClause ? $this->walkHavingClause($AST->havingClause) : ''; + + if (($orderByClause = $AST->orderByClause) !== null) { + $sql .= $AST->orderByClause ? $this->walkOrderByClause($AST->orderByClause) : ''; + } else if (($orderBySql = $this->_generateOrderedCollectionOrderByItems()) !== '') { + $sql .= ' ORDER BY '.$orderBySql; + } + + + $sql = $this->_platform->modifyLimitQuery( + $sql, $this->_query->getMaxResults(), $this->_query->getFirstResult() + ); + + if (($lockMode = $this->_query->getHint(Query::HINT_LOCK_MODE)) !== false) { + if ($lockMode == LockMode::PESSIMISTIC_READ) { + $sql .= " " . $this->_platform->getReadLockSQL(); + } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) { + $sql .= " " . $this->_platform->getWriteLockSQL(); + } else if ($lockMode == LockMode::OPTIMISTIC) { + foreach ($this->_selectedClasses AS $class) { + if ( ! $class->isVersioned) { + throw \Doctrine\ORM\OptimisticLockException::lockFailed(); + } + } + } + } + + return $sql; + } + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $AST) + { + $this->_useSqlTableAliases = false; + $sql = $this->walkUpdateClause($AST->updateClause); + + if (($whereClause = $AST->whereClause) !== null) { + $sql .= $this->walkWhereClause($whereClause); + } else if (($discSql = $this->_generateDiscriminatorColumnConditionSQL($this->_rootAliases)) !== '') { + $sql .= ' WHERE ' . $discSql; + } + + return $sql; + } + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $AST) + { + $this->_useSqlTableAliases = false; + $sql = $this->walkDeleteClause($AST->deleteClause); + + if (($whereClause = $AST->whereClause) !== null) { + $sql .= $this->walkWhereClause($whereClause); + } else if (($discSql = $this->_generateDiscriminatorColumnConditionSQL($this->_rootAliases)) !== '') { + $sql .= ' WHERE ' . $discSql; + } + + return $sql; + } + + + /** + * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. + * + * @param string $identificationVariable + * @param string $fieldName + * @return string The SQL. + */ + public function walkIdentificationVariable($identificationVariable, $fieldName = null) + { + $class = $this->_queryComponents[$identificationVariable]['metadata']; + + if ( + $fieldName !== null && $class->isInheritanceTypeJoined() && + isset($class->fieldMappings[$fieldName]['inherited']) + ) { + $class = $this->_em->getClassMetadata($class->fieldMappings[$fieldName]['inherited']); + } + + return $this->getSQLTableAlias($class->table['name'], $identificationVariable); + } + + /** + * Walks down a PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkPathExpression($pathExpr) + { + $sql = ''; + + switch ($pathExpr->type) { + case AST\PathExpression::TYPE_STATE_FIELD: + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->_queryComponents[$dqlAlias]['metadata']; + + if ($this->_useSqlTableAliases) { + $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; + } + + $sql .= $class->getQuotedColumnName($fieldName, $this->_platform); + break; + + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + // 1- the owning side: + // Just use the foreign key, i.e. u.group_id + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->_queryComponents[$dqlAlias]['metadata']; + + if (isset($class->associationMappings[$fieldName]['inherited'])) { + $class = $this->_em->getClassMetadata($class->associationMappings[$fieldName]['inherited']); + } + + $assoc = $class->associationMappings[$fieldName]; + + if ($assoc['isOwningSide']) { + // COMPOSITE KEYS NOT (YET?) SUPPORTED + if (count($assoc['sourceToTargetKeyColumns']) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + + if ($this->_useSqlTableAliases) { + $sql .= $this->getSqlTableAlias($class->table['name'], $dqlAlias) . '.'; + } + + $sql .= reset($assoc['targetToSourceKeyColumns']); + } else { + throw QueryException::associationPathInverseSideNotSupported(); + } + break; + + default: + throw QueryException::invalidPathExpression($pathExpr); + } + + return $sql; + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @param $selectClause + * @return string The SQL. + */ + public function walkSelectClause($selectClause) + { + $sql = 'SELECT ' . (($selectClause->isDistinct) ? 'DISTINCT ' : '') . implode( + ', ', array_map(array($this, 'walkSelectExpression'), $selectClause->selectExpressions) + ); + + $addMetaColumns = ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) && + $this->_query->getHydrationMode() == Query::HYDRATE_OBJECT + || + $this->_query->getHydrationMode() != Query::HYDRATE_OBJECT && + $this->_query->getHint(Query::HINT_INCLUDE_META_COLUMNS); + + foreach ($this->_selectedClasses as $dqlAlias => $class) { + // Register as entity or joined entity result + if ($this->_queryComponents[$dqlAlias]['relation'] === null) { + $this->_rsm->addEntityResult($class->name, $dqlAlias); + } else { + $this->_rsm->addJoinedEntityResult( + $class->name, $dqlAlias, + $this->_queryComponents[$dqlAlias]['parent'], + $this->_queryComponents[$dqlAlias]['relation']['fieldName'] + ); + } + + if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { + // Add discriminator columns to SQL + $rootClass = $this->_em->getClassMetadata($class->rootEntityName); + $tblAlias = $this->getSqlTableAlias($rootClass->table['name'], $dqlAlias); + $discrColumn = $rootClass->discriminatorColumn; + $columnAlias = $this->getSqlColumnAlias($discrColumn['name']); + $sql .= ", $tblAlias." . $discrColumn['name'] . ' AS ' . $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); + $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $discrColumn['fieldName']); + + // Add foreign key columns to SQL, if necessary + if ($addMetaColumns) { + //FIXME: Include foreign key columns of child classes also!!?? + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + if (isset($assoc['inherited'])) { + $owningClass = $this->_em->getClassMetadata($assoc['inherited']); + $sqlTableAlias = $this->getSqlTableAlias($owningClass->table['name'], $dqlAlias); + } else { + $sqlTableAlias = $this->getSqlTableAlias($class->table['name'], $dqlAlias); + } + + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $this->getSqlColumnAlias($srcColumn); + $sql .= ", $sqlTableAlias." . $srcColumn . ' AS ' . $columnAlias; + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + } + } + } else { + // Add foreign key columns to SQL, if necessary + if ($addMetaColumns) { + $sqlTableAlias = $this->getSqlTableAlias($class->table['name'], $dqlAlias); + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + $columnAlias = $this->getSqlColumnAlias($srcColumn); + $sql .= ', ' . $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + } + } + } + } + + return $sql; + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFromClause($fromClause) + { + $identificationVarDecls = $fromClause->identificationVariableDeclarations; + $sqlParts = array(); + + foreach ($identificationVarDecls as $identificationVariableDecl) { + $sql = ''; + + $rangeDecl = $identificationVariableDecl->rangeVariableDeclaration; + $dqlAlias = $rangeDecl->aliasIdentificationVariable; + + $this->_rootAliases[] = $dqlAlias; + + $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); + $sql .= $class->getQuotedTableName($this->_platform) . ' ' + . $this->getSqlTableAlias($class->table['name'], $dqlAlias); + + if ($class->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); + } + + foreach ($identificationVariableDecl->joinVariableDeclarations as $joinVarDecl) { + $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); + } + + if ($identificationVariableDecl->indexBy) { + $this->_rsm->addIndexBy( + $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, + $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field + ); + } + + $sqlParts[] = $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFunction($function) + { + return $function->getSql($this); + } + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + public function walkOrderByClause($orderByClause) + { + $colSql = $this->_generateOrderedCollectionOrderByItems(); + if ($colSql != '') { + $colSql = ", ".$colSql; + } + + // OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* + return ' ORDER BY ' . implode( + ', ', array_map(array($this, 'walkOrderByItem'), $orderByClause->orderByItems) + ) . $colSql; + } + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + public function walkOrderByItem($orderByItem) + { + $sql = ''; + $expr = $orderByItem->expression; + + if ($expr instanceof AST\PathExpression) { + $sql = $this->walkPathExpression($expr); + } else { + $columnName = $this->_queryComponents[$expr]['token']['value']; + + $sql = $this->_scalarResultAliasMap[$columnName]; + } + + return $sql . ' ' . strtoupper($orderByItem->type); + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + public function walkHavingClause($havingClause) + { + return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); + } + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + public function walkJoinVariableDeclaration($joinVarDecl) + { + $join = $joinVarDecl->join; + $joinType = $join->joinType; + + if ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) { + $sql = ' LEFT JOIN '; + } else { + $sql = ' INNER JOIN '; + } + + $joinAssocPathExpr = $join->joinAssociationPathExpression; + $joinedDqlAlias = $join->aliasIdentificationVariable; + $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; + $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); + $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); + $targetTableName = $targetClass->getQuotedTableName($this->_platform); + $targetTableAlias = $this->getSqlTableAlias($targetClass->table['name'], $joinedDqlAlias); + $sourceTableAlias = $this->getSqlTableAlias($sourceClass->table['name'], $joinAssocPathExpr->identificationVariable); + + // Ensure we got the owning side, since it has all mapping info + $assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; + + if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true) { + if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + } + } + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; + $first = true; + + foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + if ($relation['isOwningSide']) { + $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); + $sql .= $sourceTableAlias . '.' . $sourceColumn + . ' = ' + . $targetTableAlias . '.' . $quotedTargetColumn; + } else { + $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); + $sql .= $sourceTableAlias . '.' . $quotedTargetColumn + . ' = ' + . $targetTableAlias . '.' . $sourceColumn; + } + } + } else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { + // Join relation table + $joinTable = $assoc['joinTable']; + $joinTableAlias = $this->getSqlTableAlias($joinTable['name'], $joinedDqlAlias); + $sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON '; + + $first = true; + if ($relation['isOwningSide']) { + foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + $sql .= $sourceTableAlias . '.' . $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform) + . ' = ' + . $joinTableAlias . '.' . $relationColumn; + } + } else { + foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + $sql .= $sourceTableAlias . '.' . $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform) + . ' = ' + . $joinTableAlias . '.' . $relationColumn; + } + } + + // Join target table + $sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) + ? ' LEFT JOIN ' : ' INNER JOIN '; + $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; + + $first = true; + if ($relation['isOwningSide']) { + foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + $sql .= $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform) + . ' = ' + . $joinTableAlias . '.' . $relationColumn; + } + } else { + foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { + if ( ! $first) $sql .= ' AND '; else $first = false; + + $sql .= $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform) + . ' = ' + . $joinTableAlias . '.' . $relationColumn; + } + } + } + + // Handle WITH clause + if (($condExpr = $join->conditionalExpression) !== null) { + // Phase 2 AST optimization: Skip processment of ConditionalExpression + // if only one ConditionalTerm is defined + $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; + } + + $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); + + if ($discrSql) { + $sql .= ' AND ' . $discrSql; + } + + // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) + if ($targetClass->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); + } + + return $sql; + } + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + public function walkSelectExpression($selectExpression) + { + $sql = ''; + $expr = $selectExpression->expression; + + if ($expr instanceof AST\PathExpression) { + if ($expr->type == AST\PathExpression::TYPE_STATE_FIELD) { + $fieldName = $expr->field; + $dqlAlias = $expr->identificationVariable; + $qComp = $this->_queryComponents[$dqlAlias]; + $class = $qComp['metadata']; + + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $fieldName; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + if ($class->isInheritanceTypeJoined()) { + $tableName = $this->_em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName); + } else { + $tableName = $class->getTableName(); + } + + $sqlTableAlias = $this->getSqlTableAlias($tableName, $dqlAlias); + $columnName = $class->getQuotedColumnName($fieldName, $this->_platform); + + $columnAlias = $this->getSqlColumnAlias($columnName); + $sql .= $sqlTableAlias . '.' . $columnName . ' AS ' . $columnAlias; + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } else { + throw QueryException::invalidPathExpression($expr->type); + } + } + else if ($expr instanceof AST\AggregateExpression) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkAggregateExpression($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + else if ($expr instanceof AST\Subselect) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= '(' . $this->walkSubselect($expr) . ') AS '.$columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + else if ($expr instanceof AST\Functions\FunctionNode) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } + else if ( + $expr instanceof AST\SimpleArithmeticExpression || + $expr instanceof AST\ArithmeticTerm || + $expr instanceof AST\ArithmeticFactor || + $expr instanceof AST\ArithmeticPrimary + ) { + if ( ! $selectExpression->fieldIdentificationVariable) { + $resultAlias = $this->_scalarResultCounter++; + } else { + $resultAlias = $selectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$resultAlias] = $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addScalarResult($columnAlias, $resultAlias); + } else { + // IdentificationVariable or PartialObjectExpression + if ($expr instanceof AST\PartialObjectExpression) { + $dqlAlias = $expr->identificationVariable; + $partialFieldSet = $expr->partialFieldSet; + } else { + $dqlAlias = $expr; + $partialFieldSet = array(); + } + + $queryComp = $this->_queryComponents[$dqlAlias]; + $class = $queryComp['metadata']; + + if ( ! isset($this->_selectedClasses[$dqlAlias])) { + $this->_selectedClasses[$dqlAlias] = $class; + } + + $beginning = true; + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + if ($partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + continue; + } + + if (isset($mapping['inherited'])) { + $tableName = $this->_em->getClassMetadata($mapping['inherited'])->table['name']; + } else { + $tableName = $class->table['name']; + } + + if ($beginning) $beginning = false; else $sql .= ', '; + + $sqlTableAlias = $this->getSqlTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $class->getQuotedColumnName($fieldName, $this->_platform) + . ' AS ' . $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); + } + + // Add any additional fields of subclasses (excluding inherited fields) + // 1) on Single Table Inheritance: always, since its marginal overhead + // 2) on Class Table Inheritance only if partial objects are disallowed, + // since it requires outer joining subtables. + if ($class->isInheritanceTypeSingleTable() || ! $this->_query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) { + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSqlTableAlias($subClass->table['name'], $dqlAlias); + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping['inherited']) || $partialFieldSet && !in_array($fieldName, $partialFieldSet)) { + continue; + } + + if ($beginning) $beginning = false; else $sql .= ', '; + + $columnAlias = $this->getSqlColumnAlias($mapping['columnName']); + $sql .= $sqlTableAlias . '.' . $subClass->getQuotedColumnName($fieldName, $this->_platform) + . ' AS ' . $columnAlias; + + $columnAlias = $this->_platform->getSQLResultCasing($columnAlias); + $this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + } + + // Add join columns (foreign keys) of the subclass + //TODO: Probably better do this in walkSelectClause to honor the INCLUDE_META_COLUMNS hint + foreach ($subClass->associationMappings as $fieldName => $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE && ! isset($assoc['inherited'])) { + foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) { + if ($beginning) $beginning = false; else $sql .= ', '; + $columnAlias = $this->getSqlColumnAlias($srcColumn); + $sql .= $sqlTableAlias . '.' . $srcColumn . ' AS ' . $columnAlias; + $this->_rsm->addMetaResult($dqlAlias, $this->_platform->getSQLResultCasing($columnAlias), $srcColumn); + } + } + } + } + } + } + + return $sql; + } + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + public function walkQuantifiedExpression($qExpr) + { + return ' ' . strtoupper($qExpr->type) + . '(' . $this->walkSubselect($qExpr->subselect) . ')'; + } + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + public function walkSubselect($subselect) + { + $useAliasesBefore = $this->_useSqlTableAliases; + $this->_useSqlTableAliases = true; + + $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); + $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); + $sql .= $subselect->whereClause ? $this->walkWhereClause($subselect->whereClause) : ''; + $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; + $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; + $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; + + $this->_useSqlTableAliases = $useAliasesBefore; + + return $sql; + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + public function walkSubselectFromClause($subselectFromClause) + { + $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; + $sqlParts = array (); + + foreach ($identificationVarDecls as $subselectIdVarDecl) { + $sql = ''; + + $rangeDecl = $subselectIdVarDecl->rangeVariableDeclaration; + $dqlAlias = $rangeDecl->aliasIdentificationVariable; + + $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); + $sql .= $class->getQuotedTableName($this->_platform) . ' ' + . $this->getSqlTableAlias($class->table['name'], $dqlAlias); + + if ($class->isInheritanceTypeJoined()) { + $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); + } + + foreach ($subselectIdVarDecl->joinVariableDeclarations as $joinVarDecl) { + $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); + } + + $sqlParts[] = $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + public function walkSimpleSelectClause($simpleSelectClause) + { + return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') + . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); + } + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + public function walkSimpleSelectExpression($simpleSelectExpression) + { + $sql = ''; + $expr = $simpleSelectExpression->expression; + + if ($expr instanceof AST\PathExpression) { + $sql .= $this->walkPathExpression($expr); + } else if ($expr instanceof AST\AggregateExpression) { + if ( ! $simpleSelectExpression->fieldIdentificationVariable) { + $alias = $this->_scalarResultCounter++; + } else { + $alias = $simpleSelectExpression->fieldIdentificationVariable; + } + + $sql .= $this->walkAggregateExpression($expr) . ' AS dctrn__' . $alias; + } else if ($expr instanceof AST\Subselect) { + if ( ! $simpleSelectExpression->fieldIdentificationVariable) { + $alias = $this->_scalarResultCounter++; + } else { + $alias = $simpleSelectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + $this->_scalarResultAliasMap[$alias] = $columnAlias; + } else if ($expr instanceof AST\Functions\FunctionNode) { + if ( ! $simpleSelectExpression->fieldIdentificationVariable) { + $alias = $this->_scalarResultCounter++; + } else { + $alias = $simpleSelectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkFunction($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$alias] = $columnAlias; + } else if ( + $expr instanceof AST\SimpleArithmeticExpression || + $expr instanceof AST\ArithmeticTerm || + $expr instanceof AST\ArithmeticFactor || + $expr instanceof AST\ArithmeticPrimary + ) { + if ( ! $simpleSelectExpression->fieldIdentificationVariable) { + $alias = $this->_scalarResultCounter++; + } else { + $alias = $simpleSelectExpression->fieldIdentificationVariable; + } + + $columnAlias = 'sclr' . $this->_aliasCounter++; + $sql .= $this->walkSimpleArithmeticExpression($expr) . ' AS ' . $columnAlias; + $this->_scalarResultAliasMap[$alias] = $columnAlias; + } else { + // IdentificationVariable + $class = $this->_queryComponents[$expr]['metadata']; + $tableAlias = $this->getSqlTableAlias($class->getTableName(), $expr); + $first = true; + + foreach ($class->identifier as $identifier) { + if ($first) $first = false; else $sql .= ', '; + $sql .= $tableAlias . '.' . $class->getQuotedColumnName($identifier, $this->_platform); + } + } + + return ' ' . $sql; + } + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + public function walkAggregateExpression($aggExpression) + { + return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') + . $this->walkPathExpression($aggExpression->pathExpression) . ')'; + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + public function walkGroupByClause($groupByClause) + { + return ' GROUP BY ' . implode( + ', ', array_map(array($this, 'walkGroupByItem'), $groupByClause->groupByItems) + ); + } + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + public function walkGroupByItem(AST\PathExpression $pathExpr) + { + return $this->walkPathExpression($pathExpr); + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause) + { + $sql = 'DELETE FROM '; + $class = $this->_em->getClassMetadata($deleteClause->abstractSchemaName); + $sql .= $class->getQuotedTableName($this->_platform); + + if ($this->_useSqlTableAliases) { + $sql .= ' ' . $this->getSqlTableAlias($class->getTableName()); + } + + $this->_rootAliases[] = $deleteClause->aliasIdentificationVariable; + + return $sql; + } + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + public function walkUpdateClause($updateClause) + { + $sql = 'UPDATE '; + $class = $this->_em->getClassMetadata($updateClause->abstractSchemaName); + $sql .= $class->getQuotedTableName($this->_platform); + + if ($this->_useSqlTableAliases) { + $sql .= ' ' . $this->getSqlTableAlias($class->getTableName()); + } + + $this->_rootAliases[] = $updateClause->aliasIdentificationVariable; + + $sql .= ' SET ' . implode( + ', ', array_map(array($this, 'walkUpdateItem'), $updateClause->updateItems) + ); + + return $sql; + } + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + public function walkUpdateItem($updateItem) + { + $useTableAliasesBefore = $this->_useSqlTableAliases; + $this->_useSqlTableAliases = false; + + $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; + + $newValue = $updateItem->newValue; + + if ($newValue === null) { + $sql .= 'NULL'; + } else if ($newValue instanceof AST\Node) { + $sql .= $newValue->dispatch($this); + } else { + $sql .= $this->_conn->quote($newValue); + } + + $this->_useSqlTableAliases = $useTableAliasesBefore; + + return $sql; + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + public function walkWhereClause($whereClause) + { + $discrSql = $this->_generateDiscriminatorColumnConditionSql($this->_rootAliases); + $condSql = $this->walkConditionalExpression($whereClause->conditionalExpression); + + return ' WHERE ' . (( ! $discrSql) ? $condSql : '(' . $condSql . ') AND ' . $discrSql); + } + + /** + * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. + * + * @param ConditionalExpression + * @return string The SQL. + */ + public function walkConditionalExpression($condExpr) + { + // Phase 2 AST optimization: Skip processment of ConditionalExpression + // if only one ConditionalTerm is defined + return ( ! ($condExpr instanceof AST\ConditionalExpression)) + ? $this->walkConditionalTerm($condExpr) + : implode( + ' OR ', array_map(array($this, 'walkConditionalTerm'), $condExpr->conditionalTerms) + ); + } + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + public function walkConditionalTerm($condTerm) + { + // Phase 2 AST optimization: Skip processment of ConditionalTerm + // if only one ConditionalFactor is defined + return ( ! ($condTerm instanceof AST\ConditionalTerm)) + ? $this->walkConditionalFactor($condTerm) + : implode( + ' AND ', array_map(array($this, 'walkConditionalFactor'), $condTerm->conditionalFactors) + ); + } + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + public function walkConditionalFactor($factor) + { + // Phase 2 AST optimization: Skip processment of ConditionalFactor + // if only one ConditionalPrimary is defined + return ( ! ($factor instanceof AST\ConditionalFactor)) + ? $this->walkConditionalPrimary($factor) + : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); + } + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + * + * @param ConditionalPrimary + * @return string The SQL. + */ + public function walkConditionalPrimary($primary) + { + if ($primary->isSimpleConditionalExpression()) { + return $primary->simpleConditionalExpression->dispatch($this); + } else if ($primary->isConditionalExpression()) { + $condExpr = $primary->conditionalExpression; + + return '(' . $this->walkConditionalExpression($condExpr) . ')'; + } + } + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + public function walkExistsExpression($existsExpr) + { + $sql = ($existsExpr->not) ? 'NOT ' : ''; + + $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; + + return $sql; + } + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + public function walkCollectionMemberExpression($collMemberExpr) + { + $sql = $collMemberExpr->not ? 'NOT ' : ''; + $sql .= 'EXISTS (SELECT 1 FROM '; + $entityExpr = $collMemberExpr->entityExpression; + $collPathExpr = $collMemberExpr->collectionValuedPathExpression; + + $fieldName = $collPathExpr->field; + $dqlAlias = $collPathExpr->identificationVariable; + + $class = $this->_queryComponents[$dqlAlias]['metadata']; + + if ($entityExpr instanceof AST\InputParameter) { + $dqlParamKey = $entityExpr->name; + $entity = $this->_query->getParameter($dqlParamKey); + } else { + //TODO + throw new \BadMethodCallException("Not implemented"); + } + + $assoc = $class->associationMappings[$fieldName]; + + if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + $targetTableAlias = $this->getSqlTableAlias($targetClass->table['name']); + $sourceTableAlias = $this->getSqlTableAlias($class->table['name'], $dqlAlias); + + $sql .= $targetClass->getQuotedTableName($this->_platform) + . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc['mappedBy']]; + + $first = true; + + foreach ($owningAssoc['targetToSourceKeyColumns'] as $targetColumn => $sourceColumn) { + if ($first) $first = false; else $sql .= ' AND '; + + $sql .= $sourceTableAlias . '.' . $class->getQuotedColumnName($class->fieldNames[$targetColumn], $this->_platform) + . ' = ' + . $targetTableAlias . '.' . $sourceColumn; + } + + $sql .= ' AND '; + $first = true; + + foreach ($targetClass->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= $targetTableAlias . '.' + . $targetClass->getQuotedColumnName($idField, $this->_platform) . ' = ?'; + } + } else { // many-to-many + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + + $owningAssoc = $assoc['isOwningSide'] ? $assoc : $targetClass->associationMappings[$assoc['mappedBy']]; + $joinTable = $owningAssoc['joinTable']; + + // SQL table aliases + $joinTableAlias = $this->getSqlTableAlias($joinTable['name']); + $targetTableAlias = $this->getSqlTableAlias($targetClass->table['name']); + $sourceTableAlias = $this->getSqlTableAlias($class->table['name'], $dqlAlias); + + // join to target table + $sql .= $targetClass->getQuotedJoinTableName($owningAssoc, $this->_platform) + . ' ' . $joinTableAlias . ' INNER JOIN ' + . $targetClass->getQuotedTableName($this->_platform) + . ' ' . $targetTableAlias . ' ON '; + + // join conditions + $joinColumns = $assoc['isOwningSide'] + ? $joinTable['inverseJoinColumns'] + : $joinTable['joinColumns']; + + $first = true; + foreach ($joinColumns as $joinColumn) { + if ($first) $first = false; else $sql .= ' AND '; + + $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' + . $targetTableAlias . '.' . $targetClass->getQuotedColumnName( + $targetClass->fieldNames[$joinColumn['referencedColumnName']], + $this->_platform); + } + + $sql .= ' WHERE '; + + $joinColumns = $assoc['isOwningSide'] + ? $joinTable['joinColumns'] + : $joinTable['inverseJoinColumns']; + + $first = true; + foreach ($joinColumns as $joinColumn) { + if ($first) $first = false; else $sql .= ' AND '; + + $sql .= $joinTableAlias . '.' . $joinColumn['name'] . ' = ' + . $sourceTableAlias . '.' . $class->getQuotedColumnName( + $class->fieldNames[$joinColumn['referencedColumnName']], + $this->_platform); + } + + $sql .= ' AND '; + $first = true; + + foreach ($targetClass->identifier as $idField) { + if ($first) $first = false; else $sql .= ' AND '; + + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= $targetTableAlias . '.' + . $targetClass->getQuotedColumnName($idField, $this->_platform) . ' = ?'; + } + } + + return $sql . ')'; + } + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param EmptyCollectionComparisonExpression + * @return string The SQL. + */ + public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr) + { + $sizeFunc = new AST\Functions\SizeFunction('size'); + $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; + + return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); + } + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + public function walkNullComparisonExpression($nullCompExpr) + { + $sql = ''; + $innerExpr = $nullCompExpr->expression; + + if ($innerExpr instanceof AST\InputParameter) { + $dqlParamKey = $innerExpr->name; + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= ' ?'; + } else { + $sql .= $this->walkPathExpression($innerExpr); + } + + $sql .= ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; + + return $sql; + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + public function walkInExpression($inExpr) + { + $sql = $this->walkPathExpression($inExpr->pathExpression) + . ($inExpr->not ? ' NOT' : '') . ' IN ('; + + if ($inExpr->subselect) { + $sql .= $this->walkSubselect($inExpr->subselect); + } else { + $sql .= implode(', ', array_map(array($this, 'walkInParameter'), $inExpr->literals)); + } + + $sql .= ')'; + + return $sql; + } + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + public function walkInstanceOfExpression($instanceOfExpr) + { + $sql = ''; + + $dqlAlias = $instanceOfExpr->identificationVariable; + $discrClass = $class = $this->_queryComponents[$dqlAlias]['metadata']; + $fieldName = null; + + if ($class->discriminatorColumn) { + $discrClass = $this->_em->getClassMetadata($class->rootEntityName); + } + + if ($this->_useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($discrClass->table['name'], $dqlAlias) . '.'; + } + + $sql .= $class->discriminatorColumn['name'] . ($instanceOfExpr->not ? ' <> ' : ' = '); + + if ($instanceOfExpr->value instanceof AST\InputParameter) { + // We need to modify the parameter value to be its correspondent mapped value + $dqlParamKey = $instanceOfExpr->value->name; + $paramValue = $this->_query->getParameter($dqlParamKey); + + if ( ! ($paramValue instanceof \Doctrine\ORM\Mapping\ClassMetadata)) { + throw QueryException::invalidParameterType('ClassMetadata', get_class($paramValue)); + } + + $entityClassName = $paramValue->name; + } else { + // Get name from ClassMetadata to resolve aliases. + $entityClassName = $this->_em->getClassMetadata($instanceOfExpr->value)->name; + } + + if ($entityClassName == $class->name) { + $sql .= $this->_conn->quote($class->discriminatorValue); + } else { + $discrMap = array_flip($class->discriminatorMap); + $sql .= $this->_conn->quote($discrMap[$entityClassName]); + } + + return $sql; + } + + /** + * Walks down an InParameter AST node, thereby generating the appropriate SQL. + * + * @param InParameter + * @return string The SQL. + */ + public function walkInParameter($inParam) + { + return $inParam instanceof AST\InputParameter ? + $this->walkInputParameter($inParam) : + $this->walkLiteral($inParam); + } + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkLiteral($literal) + { + switch ($literal->type) { + case AST\Literal::STRING: + return $this->_conn->quote($literal->value); + case AST\Literal::BOOLEAN: + $bool = strtolower($literal->value) == 'true' ? true : false; + $boolVal = $this->_conn->getDatabasePlatform()->convertBooleans($bool); + return is_string($boolVal) ? $this->_conn->quote($boolVal) : $boolVal; + case AST\Literal::NUMERIC: + return $literal->value; + default: + throw QueryException::invalidLiteral($literal); + } + } + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + public function walkBetweenExpression($betweenExpr) + { + $sql = $this->walkArithmeticExpression($betweenExpr->expression); + + if ($betweenExpr->not) $sql .= ' NOT'; + + $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) + . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); + + return $sql; + } + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + public function walkLikeExpression($likeExpr) + { + $stringExpr = $likeExpr->stringExpression; + $sql = $stringExpr->dispatch($this) . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; + + if ($likeExpr->stringPattern instanceof AST\InputParameter) { + $inputParam = $likeExpr->stringPattern; + $dqlParamKey = $inputParam->name; + $this->_parserResult->addParameterMapping($dqlParamKey, $this->_sqlParamIndex++); + $sql .= '?'; + } else { + $sql .= $this->_conn->quote($likeExpr->stringPattern); + } + + if ($likeExpr->escapeChar) { + $sql .= ' ESCAPE ' . $this->_conn->quote($likeExpr->escapeChar); + } + + return $sql; + } + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + public function walkStateFieldPathExpression($stateFieldPathExpression) + { + return $this->walkPathExpression($stateFieldPathExpression); + } + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + public function walkComparisonExpression($compExpr) + { + $sql = ''; + $leftExpr = $compExpr->leftExpression; + $rightExpr = $compExpr->rightExpression; + + if ($leftExpr instanceof AST\Node) { + $sql .= $leftExpr->dispatch($this); + } else { + $sql .= is_numeric($leftExpr) ? $leftExpr : $this->_conn->quote($leftExpr); + } + + $sql .= ' ' . $compExpr->operator . ' '; + + if ($rightExpr instanceof AST\Node) { + $sql .= $rightExpr->dispatch($this); + } else { + $sql .= is_numeric($rightExpr) ? $rightExpr : $this->_conn->quote($rightExpr); + } + + return $sql; + } + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + public function walkInputParameter($inputParam) + { + $this->_parserResult->addParameterMapping($inputParam->name, $this->_sqlParamIndex++); + + return '?'; + } + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + public function walkArithmeticExpression($arithmeticExpr) + { + return ($arithmeticExpr->isSimpleArithmeticExpression()) + ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) + : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; + } + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + public function walkSimpleArithmeticExpression($simpleArithmeticExpr) + { + return ( ! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) + ? $this->walkArithmeticTerm($simpleArithmeticExpr) + : implode( + ' ', array_map(array($this, 'walkArithmeticTerm'), $simpleArithmeticExpr->arithmeticTerms) + ); + } + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticTerm($term) + { + if (is_string($term)) { + return $term; + } + + // Phase 2 AST optimization: Skip processment of ArithmeticTerm + // if only one ArithmeticFactor is defined + return ( ! ($term instanceof AST\ArithmeticTerm)) + ? $this->walkArithmeticFactor($term) + : implode( + ' ', array_map(array($this, 'walkArithmeticFactor'), $term->arithmeticFactors) + ); + } + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticFactor($factor) + { + if (is_string($factor)) { + return $factor; + } + + // Phase 2 AST optimization: Skip processment of ArithmeticFactor + // if only one ArithmeticPrimary is defined + return ( ! ($factor instanceof AST\ArithmeticFactor)) + ? $this->walkArithmeticPrimary($factor) + : ($factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : '')) + . $this->walkArithmeticPrimary($factor->arithmeticPrimary); + } + + /** + * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticPrimary($primary) + { + if ($primary instanceof AST\SimpleArithmeticExpression) { + return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; + } else if ($primary instanceof AST\Node) { + return $primary->dispatch($this); + } + + // TODO: We need to deal with IdentificationVariable here + return ''; + } + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkStringPrimary($stringPrimary) + { + return (is_string($stringPrimary)) + ? $this->_conn->quote($stringPrimary) + : $stringPrimary->dispatch($this); + } +} diff --git a/Doctrine/ORM/Query/TreeWalker.php b/Doctrine/ORM/Query/TreeWalker.php new file mode 100644 index 0000000..4bbe963 --- /dev/null +++ b/Doctrine/ORM/Query/TreeWalker.php @@ -0,0 +1,403 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * Interface for walkers of DQL ASTs (abstract syntax trees). + * + * @author Roman Borschel + * @since 2.0 + */ +interface TreeWalker +{ + /** + * Initializes TreeWalker with important information about the ASTs to be walked + * + * @param Query $query The parsed Query. + * @param ParserResult $parserResult The result of the parsing process. + * @param array $queryComponents Query components (symbol table) + */ + public function __construct($query, $parserResult, array $queryComponents); + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkSelectStatement(AST\SelectStatement $AST); + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkSelectClause($selectClause); + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkFromClause($fromClause); + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + function walkFunction($function); + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + function walkOrderByClause($orderByClause); + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + function walkOrderByItem($orderByItem); + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + function walkHavingClause($havingClause); + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + function walkJoinVariableDeclaration($joinVarDecl); + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + function walkSelectExpression($selectExpression); + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + function walkQuantifiedExpression($qExpr); + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + function walkSubselect($subselect); + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + function walkSubselectFromClause($subselectFromClause); + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + function walkSimpleSelectClause($simpleSelectClause); + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + function walkSimpleSelectExpression($simpleSelectExpression); + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + function walkAggregateExpression($aggExpression); + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + function walkGroupByClause($groupByClause); + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + function walkGroupByItem(AST\PathExpression $pathExpr); + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + function walkUpdateStatement(AST\UpdateStatement $AST); + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + function walkDeleteStatement(AST\DeleteStatement $AST); + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + function walkDeleteClause(AST\DeleteClause $deleteClause); + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + function walkUpdateClause($updateClause); + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + function walkUpdateItem($updateItem); + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + function walkWhereClause($whereClause); + + /** + * Walks down a ConditionalExpression AST node, thereby generating the appropriate SQL. + * + * @param ConditionalExpression + * @return string The SQL. + */ + function walkConditionalExpression($condExpr); + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + function walkConditionalTerm($condTerm); + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + function walkConditionalFactor($factor); + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + * + * @param ConditionalPrimary + * @return string The SQL. + */ + function walkConditionalPrimary($primary); + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + function walkExistsExpression($existsExpr); + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + function walkCollectionMemberExpression($collMemberExpr); + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param EmptyCollectionComparisonExpression + * @return string The SQL. + */ + function walkEmptyCollectionComparisonExpression($emptyCollCompExpr); + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + function walkNullComparisonExpression($nullCompExpr); + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + function walkInExpression($inExpr); + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr); + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkLiteral($literal); + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + function walkBetweenExpression($betweenExpr); + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + function walkLikeExpression($likeExpr); + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + function walkStateFieldPathExpression($stateFieldPathExpression); + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + function walkComparisonExpression($compExpr); + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + function walkInputParameter($inputParam); + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + function walkArithmeticExpression($arithmeticExpr); + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkArithmeticTerm($term); + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkStringPrimary($stringPrimary); + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkArithmeticFactor($factor); + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + function walkSimpleArithmeticExpression($simpleArithmeticExpr); + + /** + * Walks down an PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + function walkPathExpression($pathExpr); + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @return AbstractExecutor + */ + function getExecutor($AST); +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/TreeWalkerAdapter.php b/Doctrine/ORM/Query/TreeWalkerAdapter.php new file mode 100644 index 0000000..ca2a495 --- /dev/null +++ b/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -0,0 +1,437 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * An adapter implementation of the TreeWalker interface. The methods in this class + * are empty. This class exists as convenience for creating tree walkers. + * + * @author Roman Borschel + * @since 2.0 + */ +abstract class TreeWalkerAdapter implements TreeWalker +{ + private $_query; + private $_parserResult; + private $_queryComponents; + + /** + * {@inheritdoc} + */ + public function __construct($query, $parserResult, array $queryComponents) + { + $this->_query = $query; + $this->_parserResult = $parserResult; + $this->_queryComponents = $queryComponents; + } + + /** + * @return array + */ + protected function _getQueryComponents() + { + return $this->_queryComponents; + } + + /** + * Retrieve Query Instance reponsible for the current walkers execution. + * + * @return Doctrine\ORM\Query + */ + protected function _getQuery() + { + return $this->_query; + } + + /** + * Retrieve ParserResult + * + * @return Doctrine\ORM\Query\ParserResult + */ + protected function _getParserResult() + { + return $this->_parserResult; + } + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectStatement(AST\SelectStatement $AST) {} + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectClause($selectClause) {} + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFromClause($fromClause) {} + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFunction($function) {} + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + public function walkOrderByClause($orderByClause) {} + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + public function walkOrderByItem($orderByItem) {} + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + public function walkHavingClause($havingClause) {} + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + public function walkJoinVariableDeclaration($joinVarDecl) {} + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + public function walkSelectExpression($selectExpression) {} + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + public function walkQuantifiedExpression($qExpr) {} + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + public function walkSubselect($subselect) {} + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + public function walkSubselectFromClause($subselectFromClause) {} + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + public function walkSimpleSelectClause($simpleSelectClause) {} + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + public function walkSimpleSelectExpression($simpleSelectExpression) {} + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + public function walkAggregateExpression($aggExpression) {} + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + public function walkGroupByClause($groupByClause) {} + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + public function walkGroupByItem(AST\PathExpression $pathExpr) {} + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $AST) {} + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $AST) {} + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause) {} + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + public function walkUpdateClause($updateClause) {} + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + public function walkUpdateItem($updateItem) {} + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + public function walkWhereClause($whereClause) {} + + /** + * Walks down a ConditionalExpression AST node, thereby generating the appropriate SQL. + * + * @param ConditionalExpression + * @return string The SQL. + */ + public function walkConditionalExpression($condExpr) {} + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + public function walkConditionalTerm($condTerm) {} + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + public function walkConditionalFactor($factor) {} + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + * + * @param ConditionalPrimary + * @return string The SQL. + */ + public function walkConditionalPrimary($primary) {} + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + public function walkExistsExpression($existsExpr) {} + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + public function walkCollectionMemberExpression($collMemberExpr) {} + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param EmptyCollectionComparisonExpression + * @return string The SQL. + */ + public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr) {} + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + public function walkNullComparisonExpression($nullCompExpr) {} + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + public function walkInExpression($inExpr) {} + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr) {} + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkLiteral($literal) {} + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + public function walkBetweenExpression($betweenExpr) {} + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + public function walkLikeExpression($likeExpr) {} + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + public function walkStateFieldPathExpression($stateFieldPathExpression) {} + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + public function walkComparisonExpression($compExpr) {} + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + public function walkInputParameter($inputParam) {} + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + public function walkArithmeticExpression($arithmeticExpr) {} + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticTerm($term) {} + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkStringPrimary($stringPrimary) {} + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticFactor($factor) {} + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + public function walkSimpleArithmeticExpression($simpleArithmeticExpr) {} + + /** + * Walks down an PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkPathExpression($pathExpr) {} + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @return AbstractExecutor + */ + public function getExecutor($AST) {} +} \ No newline at end of file diff --git a/Doctrine/ORM/Query/TreeWalkerChain.php b/Doctrine/ORM/Query/TreeWalkerChain.php new file mode 100644 index 0000000..1fc1977 --- /dev/null +++ b/Doctrine/ORM/Query/TreeWalkerChain.php @@ -0,0 +1,651 @@ +. + */ + +namespace Doctrine\ORM\Query; + +/** + * Represents a chain of tree walkers that modify an AST and finally emit output. + * Only the last walker in the chain can emit output. Any previous walkers can modify + * the AST to influence the final output produced by the last walker. + * + * @author Roman Borschel + * @since 2.0 + */ +class TreeWalkerChain implements TreeWalker +{ + /** The tree walkers. */ + private $_walkers = array(); + /** The original Query. */ + private $_query; + /** The ParserResult of the original query that was produced by the Parser. */ + private $_parserResult; + /** The query components of the original query (the "symbol table") that was produced by the Parser. */ + private $_queryComponents; + + /** + * @inheritdoc + */ + public function __construct($query, $parserResult, array $queryComponents) + { + $this->_query = $query; + $this->_parserResult = $parserResult; + $this->_queryComponents = $queryComponents; + } + + /** + * Adds a tree walker to the chain. + * + * @param string $walkerClass The class of the walker to instantiate. + */ + public function addTreeWalker($walkerClass) + { + $this->_walkers[] = new $walkerClass($this->_query, $this->_parserResult, $this->_queryComponents); + } + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectStatement(AST\SelectStatement $AST) + { + foreach ($this->_walkers as $walker) { + $walker->walkSelectStatement($AST); + } + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkSelectClause($selectClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkSelectClause($selectClause); + } + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFromClause($fromClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkFromClause($fromClause); + } + } + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + * + * @return string The SQL. + */ + public function walkFunction($function) + { + foreach ($this->_walkers as $walker) { + $walker->walkFunction($function); + } + } + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + * + * @param OrderByClause + * @return string The SQL. + */ + public function walkOrderByClause($orderByClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkOrderByClause($orderByClause); + } + } + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + * + * @param OrderByItem + * @return string The SQL. + */ + public function walkOrderByItem($orderByItem) + { + foreach ($this->_walkers as $walker) { + $walker->walkOrderByItem($orderByItem); + } + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + * + * @param HavingClause + * @return string The SQL. + */ + public function walkHavingClause($havingClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkHavingClause($havingClause); + } + } + + /** + * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. + * + * @param JoinVariableDeclaration $joinVarDecl + * @return string The SQL. + */ + public function walkJoinVariableDeclaration($joinVarDecl) + { + foreach ($this->_walkers as $walker) { + $walker->walkJoinVariableDeclaration($joinVarDecl); + } + } + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + * + * @param SelectExpression $selectExpression + * @return string The SQL. + */ + public function walkSelectExpression($selectExpression) + { + foreach ($this->_walkers as $walker) { + $walker->walkSelectExpression($selectExpression); + } + } + + /** + * Walks down a QuantifiedExpression AST node, thereby generating the appropriate SQL. + * + * @param QuantifiedExpression + * @return string The SQL. + */ + public function walkQuantifiedExpression($qExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkQuantifiedExpression($qExpr); + } + } + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + * + * @param Subselect + * @return string The SQL. + */ + public function walkSubselect($subselect) + { + foreach ($this->_walkers as $walker) { + $walker->walkSubselect($subselect); + } + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + * + * @param SubselectFromClause + * @return string The SQL. + */ + public function walkSubselectFromClause($subselectFromClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkSubselectFromClause($subselectFromClause); + } + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectClause + * @return string The SQL. + */ + public function walkSimpleSelectClause($simpleSelectClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkSimpleSelectClause($simpleSelectClause); + } + } + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleSelectExpression + * @return string The SQL. + */ + public function walkSimpleSelectExpression($simpleSelectExpression) + { + foreach ($this->_walkers as $walker) { + $walker->walkSimpleSelectExpression($simpleSelectExpression); + } + } + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + * + * @param AggregateExpression + * @return string The SQL. + */ + public function walkAggregateExpression($aggExpression) + { + foreach ($this->_walkers as $walker) { + $walker->walkAggregateExpression($aggExpression); + } + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + * + * @param GroupByClause + * @return string The SQL. + */ + public function walkGroupByClause($groupByClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkGroupByClause($groupByClause); + } + } + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + * + * @param GroupByItem + * @return string The SQL. + */ + public function walkGroupByItem(AST\PathExpression $pathExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkGroupByItem($pathExpr); + } + } + + /** + * Walks down an UpdateStatement AST node, thereby generating the appropriate SQL. + * + * @param UpdateStatement + * @return string The SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $AST) + { + foreach ($this->_walkers as $walker) { + $walker->walkUpdateStatement($AST); + } + } + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + * + * @param DeleteStatement + * @return string The SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $AST) + { + foreach ($this->_walkers as $walker) { + $walker->walkDeleteStatement($AST); + } + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + * + * @param DeleteClause + * @return string The SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkDeleteClause($deleteClause); + } + } + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + * + * @param UpdateClause + * @return string The SQL. + */ + public function walkUpdateClause($updateClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkUpdateClause($updateClause); + } + } + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + * + * @param UpdateItem + * @return string The SQL. + */ + public function walkUpdateItem($updateItem) + { + foreach ($this->_walkers as $walker) { + $walker->walkUpdateItem($updateItem); + } + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * @param WhereClause + * @return string The SQL. + */ + public function walkWhereClause($whereClause) + { + foreach ($this->_walkers as $walker) { + $walker->walkWhereClause($whereClause); + } + } + + /** + * Walks down a ConditionalExpression AST node, thereby generating the appropriate SQL. + * + * @param ConditionalExpression + * @return string The SQL. + */ + public function walkConditionalExpression($condExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkConditionalExpression($condExpr); + } + } + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + * + * @param ConditionalTerm + * @return string The SQL. + */ + public function walkConditionalTerm($condTerm) + { + foreach ($this->_walkers as $walker) { + $walker->walkConditionalTerm($condTerm); + } + } + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + * + * @param ConditionalFactor + * @return string The SQL. + */ + public function walkConditionalFactor($factor) + { + foreach ($this->_walkers as $walker) { + $walker->walkConditionalFactor($factor); + } + } + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + * + * @param ConditionalPrimary + * @return string The SQL. + */ + public function walkConditionalPrimary($condPrimary) + { + foreach ($this->_walkers as $walker) { + $walker->walkConditionalPrimary($condPrimary); + } + } + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + * + * @param ExistsExpression + * @return string The SQL. + */ + public function walkExistsExpression($existsExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkExistsExpression($existsExpr); + } + } + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + * + * @param CollectionMemberExpression + * @return string The SQL. + */ + public function walkCollectionMemberExpression($collMemberExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkCollectionMemberExpression($collMemberExpr); + } + } + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param EmptyCollectionComparisonExpression + * @return string The SQL. + */ + public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkEmptyCollectionComparisonExpression($emptyCollCompExpr); + } + } + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param NullComparisonExpression + * @return string The SQL. + */ + public function walkNullComparisonExpression($nullCompExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkNullComparisonExpression($nullCompExpr); + } + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + * + * @param InExpression + * @return string The SQL. + */ + public function walkInExpression($inExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkInExpression($inExpr); + } + } + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @param InstanceOfExpression + * @return string The SQL. + */ + function walkInstanceOfExpression($instanceOfExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkInstanceOfExpression($instanceOfExpr); + } + } + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkLiteral($literal) + { + foreach ($this->_walkers as $walker) { + $walker->walkLiteral($literal); + } + } + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + * + * @param BetweenExpression + * @return string The SQL. + */ + public function walkBetweenExpression($betweenExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkBetweenExpression($betweenExpr); + } + } + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + * + * @param LikeExpression + * @return string The SQL. + */ + public function walkLikeExpression($likeExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkLikeExpression($likeExpr); + } + } + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + * + * @param StateFieldPathExpression + * @return string The SQL. + */ + public function walkStateFieldPathExpression($stateFieldPathExpression) + { + foreach ($this->_walkers as $walker) { + $walker->walkStateFieldPathExpression($stateFieldPathExpression); + } + } + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + * + * @param ComparisonExpression + * @return string The SQL. + */ + public function walkComparisonExpression($compExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkComparisonExpression($compExpr); + } + } + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + * + * @param InputParameter + * @return string The SQL. + */ + public function walkInputParameter($inputParam) + { + foreach ($this->_walkers as $walker) { + $walker->walkInputParameter($inputParam); + } + } + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param ArithmeticExpression + * @return string The SQL. + */ + public function walkArithmeticExpression($arithmeticExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkArithmeticExpression($arithmeticExpr); + } + } + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticTerm($term) + { + foreach ($this->_walkers as $walker) { + $walker->walkArithmeticTerm($term); + } + } + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkStringPrimary($stringPrimary) + { + foreach ($this->_walkers as $walker) { + $walker->walkStringPrimary($stringPrimary); + } + } + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkArithmeticFactor($factor) + { + foreach ($this->_walkers as $walker) { + $walker->walkArithmeticFactor($factor); + } + } + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + * + * @param SimpleArithmeticExpression + * @return string The SQL. + */ + public function walkSimpleArithmeticExpression($simpleArithmeticExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkSimpleArithmeticExpression($simpleArithmeticExpr); + } + } + + /** + * Walks down an PathExpression AST node, thereby generating the appropriate SQL. + * + * @param mixed + * @return string The SQL. + */ + public function walkPathExpression($pathExpr) + { + foreach ($this->_walkers as $walker) { + $walker->walkPathExpression($pathExpr); + } + } + + /** + * Gets an executor that can be used to execute the result of this walker. + * + * @return AbstractExecutor + */ + public function getExecutor($AST) + {} +} \ No newline at end of file diff --git a/Doctrine/ORM/QueryBuilder.php b/Doctrine/ORM/QueryBuilder.php new file mode 100644 index 0000000..436522b --- /dev/null +++ b/Doctrine/ORM/QueryBuilder.php @@ -0,0 +1,955 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\ORM\Query\Expr; + +/** + * This class is responsible for building DQL query strings via an object oriented + * PHP interface. + * + * @since 2.0 + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class QueryBuilder +{ + /* The query types. */ + const SELECT = 0; + const DELETE = 1; + const UPDATE = 2; + + /** The builder states. */ + const STATE_DIRTY = 0; + const STATE_CLEAN = 1; + + /** + * @var EntityManager The EntityManager used by this QueryBuilder. + */ + private $_em; + + /** + * @var array The array of DQL parts collected. + */ + private $_dqlParts = array( + 'select' => array(), + 'from' => array(), + 'join' => array(), + 'set' => array(), + 'where' => null, + 'groupBy' => array(), + 'having' => null, + 'orderBy' => array() + ); + + /** + * @var integer The type of query this is. Can be select, update or delete. + */ + private $_type = self::SELECT; + + /** + * @var integer The state of the query object. Can be dirty or clean. + */ + private $_state = self::STATE_CLEAN; + + /** + * @var string The complete DQL string for this query. + */ + private $_dql; + + /** + * @var array The query parameters. + */ + private $_params = array(); + + /** + * @var array The parameter type map of this query. + */ + private $_paramTypes = array(); + + /** + * @var integer The index of the first result to retrieve. + */ + private $_firstResult = null; + + /** + * @var integer The maximum number of results to retrieve. + */ + private $_maxResults = null; + + /** + * Initializes a new QueryBuilder that uses the given EntityManager. + * + * @param EntityManager $em The EntityManager to use. + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + * + * @return Expr + */ + public function expr() + { + return $this->_em->getExpressionBuilder(); + } + + /** + * Get the type of the currently built query. + * + * @return integer + */ + public function getType() + { + return $this->_type; + } + + /** + * Get the associated EntityManager for this query builder. + * + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /** + * Get the state of this query builder instance. + * + * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. + */ + public function getState() + { + return $this->_state; + } + + /** + * Get the complete DQL string formed by the current specifications of this QueryBuilder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * echo $qb->getDql(); // SELECT u FROM User u + * + * + * @return string The DQL query string. + */ + public function getDQL() + { + if ($this->_dql !== null && $this->_state === self::STATE_CLEAN) { + return $this->_dql; + } + + $dql = ''; + + switch ($this->_type) { + case self::DELETE: + $dql = $this->_getDQLForDelete(); + break; + + case self::UPDATE: + $dql = $this->_getDQLForUpdate(); + break; + + case self::SELECT: + default: + $dql = $this->_getDQLForSelect(); + break; + } + + $this->_state = self::STATE_CLEAN; + $this->_dql = $dql; + + return $dql; + } + + /** + * Constructs a Query instance from the current specifications of the builder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * $q = $qb->getQuery(); + * $results = $q->execute(); + * + * + * @return Query + */ + public function getQuery() + { + return $this->_em->createQuery($this->getDQL()) + ->setParameters($this->_params, $this->_paramTypes) + ->setFirstResult($this->_firstResult) + ->setMaxResults($this->_maxResults); + } + + /** + * Gets the root alias of the query. This is the first entity alias involved + * in the construction of the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u'); + * + * echo $qb->getRootAlias(); // u + * + * + * @return string $rootAlias + * @todo Rename/Refactor: getRootAliases(), there can be multiple roots! + */ + public function getRootAlias() + { + return $this->_dqlParts['from'][0]->getAlias(); + } + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id') + * ->setParameter(':user_id', 1); + * + * + * @param string|integer $key The parameter position or name. + * @param mixed $value The parameter value. + * @param string|null $type PDO::PARAM_* or \Doctrine\DBAL\Types\Type::* constant + * @return QueryBuilder This QueryBuilder instance. + */ + public function setParameter($key, $value, $type = null) + { + if ($type !== null) { + $this->_paramTypes[$key] = $type; + } + $this->_params[$key] = $value; + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(array( + * ':user_id1' => 1, + * ':user_id2' => 2 + * )); + * + * + * @param array $params The query parameters to set. + * @return QueryBuilder This QueryBuilder instance. + */ + public function setParameters(array $params, array $types = array()) + { + $this->_paramTypes = $types; + $this->_params = $params; + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed. + * + * @return array The currently defined query parameters. + */ + public function getParameters() + { + return $this->_params; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param mixed $key The key (index or name) of the bound parameter. + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) + { + return isset($this->_params[$key]) ? $this->_params[$key] : null; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param integer $firstResult The first result to return. + * @return QueryBuilder This QueryBuilder instance. + */ + public function setFirstResult($firstResult) + { + $this->_firstResult = $firstResult; + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. + * + * @return integer The position of the first result. + */ + public function getFirstResult() + { + return $this->_firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param integer $maxResults The maximum number of results to retrieve. + * @return QueryBuilder This QueryBuilder instance. + */ + public function setMaxResults($maxResults) + { + $this->_maxResults = $maxResults; + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if {@link setMaxResults} was not applied to this query builder. + * + * @return integer Maximum number of results. + */ + public function getMaxResults() + { + return $this->_maxResults; + } + + /** + * Either appends to or replaces a single, generic query part. + * + * The available parts are: 'select', 'from', 'join', 'set', 'where', + * 'groupBy', 'having' and 'orderBy'. + * + * @param string $dqlPartName + * @param string $dqlPart + * @param string $append + * @return QueryBuilder This QueryBuilder instance. + */ + public function add($dqlPartName, $dqlPart, $append = false) + { + $isMultiple = is_array($this->_dqlParts[$dqlPartName]); + + if ($append && $isMultiple) { + $this->_dqlParts[$dqlPartName][] = $dqlPart; + } else { + $this->_dqlParts[$dqlPartName] = ($isMultiple) ? array($dqlPart) : $dqlPart; + } + + $this->_state = self::STATE_DIRTY; + + return $this; + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u', 'p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expressions. + * @return QueryBuilder This QueryBuilder instance. + */ + public function select($select = null) + { + $this->_type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', new Expr\Select($selects), false); + } + + /** + * Adds an item that is to be returned in the query result. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->addSelect('p') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p'); + * + * + * @param mixed $select The selection expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addSelect($select = null) + { + $this->_type = self::SELECT; + + if (empty($select)) { + return $this; + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', new Expr\Select($selects), true); + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->delete('User', 'u') + * ->where('u.id = :user_id'); + * ->setParameter(':user_id', 1); + * + * + * @param string $delete The class/type whose instances are subject to the deletion. + * @param string $alias The class/type alias used in the constructed query. + * @return QueryBuilder This QueryBuilder instance. + */ + public function delete($delete = null, $alias = null) + { + $this->_type = self::DELETE; + + if ( ! $delete) { + return $this; + } + + return $this->add('from', new Expr\From($delete, $alias)); + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain entity type. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $update The class/type whose instances are subject to the update. + * @param string $alias The class/type alias used in the constructed query. + * @return QueryBuilder This QueryBuilder instance. + */ + public function update($update = null, $alias = null) + { + $this->_type = self::UPDATE; + + if ( ! $update) { + return $this; + } + + return $this->add('from', new Expr\From($update, $alias)); + } + + /** + * Create and add a query root corresponding to the entity identified by the given alias, + * forming a cartesian product with any existing query roots. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * + * + * @param string $from The class name. + * @param string $alias The alias of the class. + * @return QueryBuilder This QueryBuilder instance. + */ + public function from($from, $alias) + { + return $this->add('from', new Expr\From($from, $alias), true); + } + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->join('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join + * @param string $alias The alias of the join + * @param string $conditionType The condition type constant. Either ON or WITH. + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function join($join, $alias, $conditionType = null, $condition = null) + { + return $this->innerJoin($join, $alias, $conditionType, $condition); + } + + /** + * Creates and adds a join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * [php] + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->innerJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * @param string $join The relationship to join + * @param string $alias The alias of the join + * @param string $conditionType The condition type constant. Either ON or WITH. + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function innerJoin($join, $alias, $conditionType = null, $condition = null) + { + return $this->add('join', new Expr\Join( + Expr\Join::INNER_JOIN, $join, $alias, $conditionType, $condition + ), true); + } + + /** + * Creates and adds a left join over an entity association to the query. + * + * The entities in the joined association will be fetched as part of the query + * result if the alias used for the joined association is placed in the select + * expressions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, 'p.is_primary = 1'); + * + * + * @param string $join The relationship to join + * @param string $alias The alias of the join + * @param string $conditionType The condition type constant. Either ON or WITH. + * @param string $condition The condition for the join + * @return QueryBuilder This QueryBuilder instance. + */ + public function leftJoin($join, $alias, $conditionType = null, $condition = null) + { + return $this->add('join', new Expr\Join( + Expr\Join::LEFT_JOIN, $join, $alias, $conditionType, $condition + ), true); + } + + /** + * Sets a new value for a field in a bulk update query. + * + * + * $qb = $em->createQueryBuilder() + * ->update('User', 'u') + * ->set('u.password', md5('password')) + * ->where('u.id = ?'); + * + * + * @param string $key The key/field to set. + * @param string $value The value, expression, placeholder, etc. + * @return QueryBuilder This QueryBuilder instance. + */ + public function set($key, $value) + { + return $this->add('set', new Expr\Comparison($key, Expr\Comparison::EQ, $value), true); + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = ?'); + * + * // You can optionally programatically build and/or expressions + * $qb = $em->createQueryBuilder(); + * + * $or = $qb->expr()->orx(); + * $or->add($qb->expr()->eq('u.id', 1)); + * $or->add($qb->expr()->eq('u.id', 2)); + * + * $qb->update('User', 'u') + * ->set('u.password', md5('password')) + * ->where($or); + * + * + * @param mixed $predicates The restriction predicates. + * @return QueryBuilder This QueryBuilder instance. + */ + public function where($predicates) + { + if ( ! (func_num_args() == 1 && ($predicates instanceof Expr\Andx || $predicates instanceof Expr\Orx))) { + $predicates = new Expr\Andx(func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @param mixed $where The query restrictions. + * @return QueryBuilder This QueryBuilder instance. + * @see where() + */ + public function andWhere($where) + { + $where = $this->getDQLPart('where'); + $args = func_get_args(); + + if ($where instanceof Expr\Andx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Andx($args); + } + + return $this->add('where', $where, true); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @param mixed $where The WHERE statement + * @return QueryBuilder $qb + * @see where() + */ + public function orWhere($where) + { + $where = $this->getDqlPart('where'); + $args = func_get_args(); + + if ($where instanceof Expr\Orx) { + $where->addMultiple($args); + } else { + array_unshift($args, $where); + $where = new Expr\Orx($args); + } + + return $this->add('where', $where, true); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.id'); + * + * + * @param string $groupBy The grouping expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function groupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args())); + } + + + /** + * Adds a grouping expression to the query. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * ->groupBy('u.lastLogin'); + * ->addGroupBy('u.createdAt') + * + * + * @param string $groupBy The grouping expression. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addGroupBy($groupBy) + { + return $this->add('groupBy', new Expr\GroupBy(func_get_args()), true); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * @return QueryBuilder This QueryBuilder instance. + */ + public function having($having) + { + if ( ! (func_num_args() == 1 && ($having instanceof Expr\Andx || $having instanceof Expr\Orx))) { + $having = new Expr\Andx(func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * @return QueryBuilder This QueryBuilder instance. + */ + public function andHaving($having) + { + $having = $this->getDqlPart('having'); + $args = func_get_args(); + + if ($having instanceof Expr\Andx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Andx($args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * @return QueryBuilder This QueryBuilder instance. + */ + public function orHaving($having) + { + $having = $this->getDqlPart('having'); + $args = func_get_args(); + + if ($having instanceof Expr\Orx) { + $having->addMultiple($args); + } else { + array_unshift($args, $having); + $having = new Expr\Orx($args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * @return QueryBuilder This QueryBuilder instance. + */ + public function orderBy($sort, $order = null) + { + return $this->add('orderBy', $sort instanceof Expr\OrderBy ? $sort + : new Expr\OrderBy($sort, $order)); + } + + /** + * Adds an ordering to the query results. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * @return QueryBuilder This QueryBuilder instance. + */ + public function addOrderBy($sort, $order = null) + { + return $this->add('orderBy', new Expr\OrderBy($sort, $order), true); + } + + /** + * Get a query part by its name. + * + * @param string $queryPartName + * @return mixed $queryPart + * @todo Rename: getQueryPart (or remove?) + */ + public function getDQLPart($queryPartName) + { + return $this->_dqlParts[$queryPartName]; + } + + /** + * Get all query parts. + * + * @return array $dqlParts + * @todo Rename: getQueryParts (or remove?) + */ + public function getDQLParts() + { + return $this->_dqlParts; + } + + private function _getDQLForDelete() + { + return 'DELETE' + . $this->_getReducedDQLQueryPart('from', array('pre' => ' ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('where', array('pre' => ' WHERE ')) + . $this->_getReducedDQLQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')); + } + + private function _getDQLForUpdate() + { + return 'UPDATE' + . $this->_getReducedDQLQueryPart('from', array('pre' => ' ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('set', array('pre' => ' SET ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('where', array('pre' => ' WHERE ')) + . $this->_getReducedDQLQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')); + } + + private function _getDQLForSelect() + { + return 'SELECT' + . $this->_getReducedDQLQueryPart('select', array('pre' => ' ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('from', array('pre' => ' FROM ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('join', array('pre' => ' ', 'separator' => ' ')) + . $this->_getReducedDQLQueryPart('where', array('pre' => ' WHERE ')) + . $this->_getReducedDQLQueryPart('groupBy', array('pre' => ' GROUP BY ', 'separator' => ', ')) + . $this->_getReducedDQLQueryPart('having', array('pre' => ' HAVING ')) + . $this->_getReducedDQLQueryPart('orderBy', array('pre' => ' ORDER BY ', 'separator' => ', ')); + } + + private function _getReducedDQLQueryPart($queryPartName, $options = array()) + { + $queryPart = $this->getDQLPart($queryPartName); + + if (empty($queryPart)) { + return (isset($options['empty']) ? $options['empty'] : ''); + } + + return (isset($options['pre']) ? $options['pre'] : '') + . (is_array($queryPart) ? implode($options['separator'], $queryPart) : $queryPart) + . (isset($options['post']) ? $options['post'] : ''); + } + + /** + * Reset DQL parts + * + * @param array $parts + * @return QueryBuilder + */ + public function resetDQLParts($parts = null) + { + if (is_null($parts)) { + $parts = array_keys($this->_dqlParts); + } + foreach ($parts as $part) { + $this->resetDQLPart($part); + } + return $this; + } + + /** + * Reset single DQL part + * + * @param string $part + * @return QueryBuilder; + */ + public function resetDQLPart($part) + { + if (is_array($this->_dqlParts[$part])) { + $this->_dqlParts[$part] = array(); + } else { + $this->_dqlParts[$part] = null; + } + $this->_state = self::STATE_DIRTY; + return $this; + } + + /** + * Gets a string representation of this QueryBuilder which corresponds to + * the final DQL query being constructed. + * + * @return string The string representation of this QueryBuilder. + */ + public function __toString() + { + return $this->getDQL(); + } + + /** + * Deep clone of all expression objects in the DQL parts. + * + * @return void + */ + public function __clone() + { + foreach ($this->_dqlParts AS $part => $elements) { + if (is_array($this->_dqlParts[$part])) { + foreach ($this->_dqlParts[$part] AS $idx => $element) { + if (is_object($element)) { + $this->_dqlParts[$part][$idx] = clone $element; + } + } + } else if (\is_object($elements)) { + $this->_dqlParts[$part] = clone $elements; + } + } + } +} \ No newline at end of file diff --git a/Doctrine/ORM/README.markdown b/Doctrine/ORM/README.markdown new file mode 100644 index 0000000..e69de29 diff --git a/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php b/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php new file mode 100644 index 0000000..0bb3189 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php @@ -0,0 +1,85 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Command to clear the metadata cache of the various cache drivers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class MetadataCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:metadata') + ->setDescription('Clear all metadata cache of the various cache drivers.') + ->setDefinition(array()) + ->setHelp(<<getHelper('em')->getEntityManager(); + $cacheDriver = $em->getConfiguration()->getMetadataCacheImpl(); + + if ( ! $cacheDriver) { + throw new \InvalidArgumentException('No Metadata cache driver is configured on given EntityManager.'); + } + + if ($cacheDriver instanceof \Doctrine\Common\Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + + $output->write('Clearing ALL Metadata cache entries' . PHP_EOL); + + $cacheIds = $cacheDriver->deleteAll(); + + if ($cacheIds) { + foreach ($cacheIds as $cacheId) { + $output->write(' - ' . $cacheId . PHP_EOL); + } + } else { + $output->write('No entries to be deleted.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php b/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php new file mode 100644 index 0000000..9b71292 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php @@ -0,0 +1,85 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Command to clear the query cache of the various cache drivers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class QueryCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:query') + ->setDescription('Clear all query cache of the various cache drivers.') + ->setDefinition(array()) + ->setHelp(<<getHelper('em')->getEntityManager(); + $cacheDriver = $em->getConfiguration()->getQueryCacheImpl(); + + if ( ! $cacheDriver) { + throw new \InvalidArgumentException('No Query cache driver is configured on given EntityManager.'); + } + + if ($cacheDriver instanceof \Doctrine\Common\Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + + $output->write('Clearing ALL Query cache entries' . PHP_EOL); + + $cacheIds = $cacheDriver->deleteAll(); + + if ($cacheIds) { + foreach ($cacheIds as $cacheId) { + $output->write(' - ' . $cacheId . PHP_EOL); + } + } else { + $output->write('No entries to be deleted.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php b/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php new file mode 100644 index 0000000..f150680 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php @@ -0,0 +1,168 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Command to clear the result cache of the various cache drivers. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ResultCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:result') + ->setDescription('Clear result cache of the various cache drivers.') + ->setDefinition(array( + new InputOption( + 'id', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'ID(s) of the cache entry to delete (accepts * wildcards).', array() + ), + new InputOption( + 'regex', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Delete cache entries that match the given regular expression(s).', array() + ), + new InputOption( + 'prefix', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Delete cache entries that have the given prefix(es).', array() + ), + new InputOption( + 'suffix', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Delete cache entries that have the given suffix(es).', array() + ), + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + $cacheDriver = $em->getConfiguration()->getResultCacheImpl(); + + if ( ! $cacheDriver) { + throw new \InvalidArgumentException('No Result cache driver is configured on given EntityManager.'); + } + + if ($cacheDriver instanceof \Doctrine\Common\Cache\ApcCache) { + throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + + $outputed = false; + + // Removing based on --id + if (($ids = $input->getOption('id')) !== null && $ids) { + foreach ($ids as $id) { + $output->write($outputed ? PHP_EOL : ''); + $output->write(sprintf('Clearing Result cache entries that match the id "%s"', $id) . PHP_EOL); + + $deleted = $cacheDriver->delete($id); + + if (is_array($deleted)) { + $this->_printDeleted($output, $deleted); + } else if (is_bool($deleted) && $deleted) { + $this->_printDeleted($output, array($id)); + } + + $outputed = true; + } + } + + // Removing based on --regex + if (($regexps = $input->getOption('regex')) !== null && $regexps) { + foreach($regexps as $regex) { + $output->write($outputed ? PHP_EOL : ''); + $output->write(sprintf('Clearing Result cache entries that match the regular expression "%s"', $regex) . PHP_EOL); + + $this->_printDeleted($output, $cacheDriver->deleteByRegex('/' . $regex. '/')); + + $outputed = true; + } + } + + // Removing based on --prefix + if (($prefixes = $input->getOption('prefix')) !== null & $prefixes) { + foreach ($prefixes as $prefix) { + $output->write($outputed ? PHP_EOL : ''); + $output->write(sprintf('Clearing Result cache entries that have the prefix "%s"', $prefix) . PHP_EOL); + + $this->_printDeleted($output, $cacheDriver->deleteByPrefix($prefix)); + + $outputed = true; + } + } + + // Removing based on --suffix + if (($suffixes = $input->getOption('suffix')) !== null && $suffixes) { + foreach ($suffixes as $suffix) { + $output->write($outputed ? PHP_EOL : ''); + $output->write(sprintf('Clearing Result cache entries that have the suffix "%s"', $suffix) . PHP_EOL); + + $this->_printDeleted($output, $cacheDriver->deleteBySuffix($suffix)); + + $outputed = true; + } + } + + // Removing ALL entries + if ( ! $ids && ! $regexps && ! $prefixes && ! $suffixes) { + $output->write($outputed ? PHP_EOL : ''); + $output->write('Clearing ALL Result cache entries' . PHP_EOL); + + $this->_printDeleted($output, $cacheDriver->deleteAll()); + + $outputed = true; + } + } + + private function _printDeleted(Console\Output\OutputInterface $output, array $items) + { + if ($items) { + foreach ($items as $item) { + $output->write(' - ' . $item . PHP_EOL); + } + } else { + $output->write('No entries to be deleted.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php b/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php new file mode 100644 index 0000000..1a1328a --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php @@ -0,0 +1,223 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + Doctrine\ORM\Tools\Export\ClassMetadataExporter, + Doctrine\ORM\Tools\ConvertDoctrine1Schema, + Doctrine\ORM\Tools\EntityGenerator; + +/** + * Command to convert a Doctrine 1 schema to a Doctrine 2 mapping file. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConvertDoctrine1SchemaCommand extends Console\Command\Command +{ + /** + * @var EntityGenerator + */ + private $entityGenerator = null; + + /** + * @var ClassMetadataExporter + */ + private $metadataExporter = null; + + /** + * @return EntityGenerator + */ + public function getEntityGenerator() + { + if ($this->entityGenerator == null) { + $this->entityGenerator = new EntityGenerator(); + } + + return $this->entityGenerator; + } + + /** + * @param EntityGenerator $entityGenerator + */ + public function setEntityGenerator(EntityGenerator $entityGenerator) + { + $this->entityGenerator = $entityGenerator; + } + + /** + * @return ClassMetadataExporter + */ + public function getMetadataExporter() + { + if ($this->metadataExporter == null) { + $this->metadataExporter = new ClassMetadataExporter(); + } + + return $this->metadataExporter; + } + + /** + * @param ClassMetadataExporter $metadataExporter + */ + public function setMetadataExporter(ClassMetadataExporter $metadataExporter) + { + $this->metadataExporter = $metadataExporter; + } + + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:convert-d1-schema') + ->setDescription('Converts Doctrine 1.X schema into a Doctrine 2.X schema.') + ->setDefinition(array( + new InputArgument( + 'from-path', InputArgument::REQUIRED, 'The path of Doctrine 1.X schema information.' + ), + new InputArgument( + 'to-type', InputArgument::REQUIRED, 'The destination Doctrine 2.X mapping type.' + ), + new InputArgument( + 'dest-path', InputArgument::REQUIRED, + 'The path to generate your Doctrine 2.X mapping information.' + ), + new InputOption( + 'from', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Optional paths of Doctrine 1.X schema information.', + array() + ), + new InputOption( + 'extend', null, InputOption::VALUE_OPTIONAL, + 'Defines a base class to be extended by generated entity classes.' + ), + new InputOption( + 'num-spaces', null, InputOption::VALUE_OPTIONAL, + 'Defines the number of indentation spaces', 4 + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + // Process source directories + $fromPaths = array_merge(array($input->getArgument('from-path')), $input->getOption('from')); + + // Process destination directory + $destPath = realpath($input->getArgument('dest-path')); + + $toType = $input->getArgument('to-type'); + $extend = $input->getOption('extend'); + $numSpaces = $input->getOption('num-spaces'); + + $this->convertDoctrine1Schema($em, $fromPaths, $destPath, $toType, $numSpaces, $extend, $output); + } + + /** + * @param \Doctrine\ORM\EntityManager $em + * @param array $fromPaths + * @param string $destPath + * @param string $toType + * @param int $numSpaces + * @param string|null $extend + * @param Console\Output\OutputInterface $output + */ + public function convertDoctrine1Schema($em, $fromPaths, $destPath, $toType, $numSpaces, $extend, $output) + { + foreach ($fromPaths as &$dirName) { + $dirName = realpath($dirName); + + if ( ! file_exists($dirName)) { + throw new \InvalidArgumentException( + sprintf("Doctrine 1.X schema directory '%s' does not exist.", $dirName) + ); + } else if ( ! is_readable($dirName)) { + throw new \InvalidArgumentException( + sprintf("Doctrine 1.X schema directory '%s' does not have read permissions.", $dirName) + ); + } + } + + if ( ! file_exists($destPath)) { + throw new \InvalidArgumentException( + sprintf("Doctrine 2.X mapping destination directory '%s' does not exist.", $destPath) + ); + } else if ( ! is_writable($destPath)) { + throw new \InvalidArgumentException( + sprintf("Doctrine 2.X mapping destination directory '%s' does not have write permissions.", $destPath) + ); + } + + $cme = $this->getMetadataExporter(); + $exporter = $cme->getExporter($toType, $destPath); + + if (strtolower($toType) === 'annotation') { + $entityGenerator = $this->getEntityGenerator(); + $exporter->setEntityGenerator($entityGenerator); + + $entityGenerator->setNumSpaces($numSpaces); + + if ($extend !== null) { + $entityGenerator->setClassToExtend($extend); + } + } + + $converter = new ConvertDoctrine1Schema($fromPaths); + $metadata = $converter->getMetadata(); + + if ($metadata) { + $output->write(PHP_EOL); + + foreach ($metadata as $class) { + $output->write(sprintf('Processing entity "%s"', $class->name) . PHP_EOL); + } + + $exporter->setMetadata($metadata); + $exporter->export(); + + $output->write(PHP_EOL . sprintf( + 'Converting Doctrine 1.X schema to "%s" mapping type in "%s"', $toType, $destPath + )); + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php b/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php new file mode 100644 index 0000000..797bc29 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php @@ -0,0 +1,151 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + Doctrine\ORM\Tools\Console\MetadataFilter, + Doctrine\ORM\Tools\Export\ClassMetadataExporter, + Doctrine\ORM\Tools\EntityGenerator, + Doctrine\ORM\Tools\DisconnectedClassMetadataFactory; + +/** + * Command to convert your mapping information between the various formats. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConvertMappingCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:convert-mapping') + ->setDescription('Convert mapping information between supported formats.') + ->setDefinition(array( + new InputOption( + 'filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'A string pattern used to match entities that should be processed.' + ), + new InputArgument( + 'to-type', InputArgument::REQUIRED, 'The mapping type to be converted.' + ), + new InputArgument( + 'dest-path', InputArgument::REQUIRED, + 'The path to generate your entities classes.' + ), + new InputOption( + 'from-database', null, null, 'Whether or not to convert mapping information from existing database.' + ), + new InputOption( + 'extend', null, InputOption::VALUE_OPTIONAL, + 'Defines a base class to be extended by generated entity classes.' + ), + new InputOption( + 'num-spaces', null, InputOption::VALUE_OPTIONAL, + 'Defines the number of indentation spaces', 4 + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + if ($input->getOption('from-database') === true) { + $em->getConfiguration()->setMetadataDriverImpl( + new \Doctrine\ORM\Mapping\Driver\DatabaseDriver( + $em->getConnection()->getSchemaManager() + ) + ); + } + + $cmf = new DisconnectedClassMetadataFactory(); + $cmf->setEntityManager($em); + $metadata = $cmf->getAllMetadata(); + $metadata = MetadataFilter::filter($metadata, $input->getOption('filter')); + + // Process destination directory + if ( ! is_dir($destPath = $input->getArgument('dest-path'))) { + mkdir($destPath, 0777, true); + } + $destPath = realpath($destPath); + + if ( ! file_exists($destPath)) { + throw new \InvalidArgumentException( + sprintf("Mapping destination directory '%s' does not exist.", $destPath) + ); + } else if ( ! is_writable($destPath)) { + throw new \InvalidArgumentException( + sprintf("Mapping destination directory '%s' does not have write permissions.", $destPath) + ); + } + + $toType = strtolower($input->getArgument('to-type')); + + $cme = new ClassMetadataExporter(); + $exporter = $cme->getExporter($toType, $destPath); + + if ($toType == 'annotation') { + $entityGenerator = new EntityGenerator(); + $exporter->setEntityGenerator($entityGenerator); + + $entityGenerator->setNumSpaces($input->getOption('num-spaces')); + + if (($extend = $input->getOption('extend')) !== null) { + $entityGenerator->setClassToExtend($extend); + } + } + + if (count($metadata)) { + foreach ($metadata as $class) { + $output->write(sprintf('Processing entity "%s"', $class->name) . PHP_EOL); + } + + $exporter->setMetadata($metadata); + $exporter->export(); + + $output->write(PHP_EOL . sprintf( + 'Exporting "%s" mapping information to "%s"' . PHP_EOL, $toType, $destPath + )); + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/EnsureProductionSettingsCommand.php b/Doctrine/ORM/Tools/Console/Command/EnsureProductionSettingsCommand.php new file mode 100644 index 0000000..d29bfce --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/EnsureProductionSettingsCommand.php @@ -0,0 +1,85 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Command to ensure that Doctrine is properly configured for a production environment. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EnsureProductionSettingsCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:ensure-production-settings') + ->setDescription('Verify that Doctrine is properly configured for a production environment.') + ->setDefinition(array( + new InputOption( + 'complete', null, InputOption::VALUE_NONE, + 'Flag to also inspect database connection existance.' + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + $error = false; + try { + $em->getConfiguration()->ensureProductionSettings(); + + if ($input->getOption('complete') !== null) { + $em->getConnection()->connect(); + } + } catch (\Exception $e) { + $error = true; + $output->writeln('' . $e->getMessage() . ''); + } + + if ($error === false) { + $output->write('Environment is correctly configured for production.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php b/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php new file mode 100644 index 0000000..f69b516 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php @@ -0,0 +1,146 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + Doctrine\ORM\Tools\Console\MetadataFilter, + Doctrine\ORM\Tools\EntityGenerator, + Doctrine\ORM\Tools\DisconnectedClassMetadataFactory; + +/** + * Command to generate entity classes and method stubs from your mapping information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class GenerateEntitiesCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:generate-entities') + ->setDescription('Generate entity classes and method stubs from your mapping information.') + ->setDefinition(array( + new InputOption( + 'filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'A string pattern used to match entities that should be processed.' + ), + new InputArgument( + 'dest-path', InputArgument::REQUIRED, 'The path to generate your entity classes.' + ), + new InputOption( + 'generate-annotations', null, InputOption::VALUE_OPTIONAL, + 'Flag to define if generator should generate annotation metadata on entities.', false + ), + new InputOption( + 'generate-methods', null, InputOption::VALUE_OPTIONAL, + 'Flag to define if generator should generate stub methods on entities.', true + ), + new InputOption( + 'regenerate-entities', null, InputOption::VALUE_OPTIONAL, + 'Flag to define if generator should regenerate entity if it exists.', false + ), + new InputOption( + 'update-entities', null, InputOption::VALUE_OPTIONAL, + 'Flag to define if generator should only update entity if it exists.', true + ), + new InputOption( + 'extend', null, InputOption::VALUE_OPTIONAL, + 'Defines a base class to be extended by generated entity classes.' + ), + new InputOption( + 'num-spaces', null, InputOption::VALUE_OPTIONAL, + 'Defines the number of indentation spaces', 4 + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + $cmf = new DisconnectedClassMetadataFactory(); + $cmf->setEntityManager($em); + $metadatas = $cmf->getAllMetadata(); + $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); + + // Process destination directory + $destPath = realpath($input->getArgument('dest-path')); + + if ( ! file_exists($destPath)) { + throw new \InvalidArgumentException( + sprintf("Entities destination directory '%s' does not exist.", $destPath) + ); + } else if ( ! is_writable($destPath)) { + throw new \InvalidArgumentException( + sprintf("Entities destination directory '%s' does not have write permissions.", $destPath) + ); + } + + if (count($metadatas)) { + // Create EntityGenerator + $entityGenerator = new EntityGenerator(); + + $entityGenerator->setGenerateAnnotations($input->getOption('generate-annotations')); + $entityGenerator->setGenerateStubMethods($input->getOption('generate-methods')); + $entityGenerator->setRegenerateEntityIfExists($input->getOption('regenerate-entities')); + $entityGenerator->setUpdateEntityIfExists($input->getOption('update-entities')); + $entityGenerator->setNumSpaces($input->getOption('num-spaces')); + + if (($extend = $input->getOption('extend')) !== null) { + $entityGenerator->setClassToExtend($extend); + } + + foreach ($metadatas as $metadata) { + $output->write( + sprintf('Processing entity "%s"', $metadata->name) . PHP_EOL + ); + } + + // Generating Entities + $entityGenerator->generate($metadatas, $destPath); + + // Outputting information message + $output->write(PHP_EOL . sprintf('Entity classes generated to "%s"', $destPath) . PHP_EOL); + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php b/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php new file mode 100644 index 0000000..9876289 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php @@ -0,0 +1,115 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + Doctrine\ORM\Tools\Console\MetadataFilter; + +/** + * Command to (re)generate the proxy classes used by doctrine. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class GenerateProxiesCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:generate-proxies') + ->setDescription('Generates proxy classes for entity classes.') + ->setDefinition(array( + new InputOption( + 'filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'A string pattern used to match entities that should be processed.' + ), + new InputArgument( + 'dest-path', InputArgument::OPTIONAL, + 'The path to generate your proxy classes. If none is provided, it will attempt to grab from configuration.' + ), + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); + + // Process destination directory + if (($destPath = $input->getArgument('dest-path')) === null) { + $destPath = $em->getConfiguration()->getProxyDir(); + } + + if ( ! is_dir($destPath)) { + mkdir($destPath, 0777, true); + } + + $destPath = realpath($destPath); + + if ( ! file_exists($destPath)) { + throw new \InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not exist.", $destPath) + ); + } else if ( ! is_writable($destPath)) { + throw new \InvalidArgumentException( + sprintf("Proxies destination directory '%s' does not have write permissions.", $destPath) + ); + } + + if ( count($metadatas)) { + foreach ($metadatas as $metadata) { + $output->write( + sprintf('Processing entity "%s"', $metadata->name) . PHP_EOL + ); + } + + // Generating Proxies + $em->getProxyFactory()->generateProxyClasses($metadatas, $destPath); + + // Outputting information message + $output->write(PHP_EOL . sprintf('Proxy classes generated to "%s"', $destPath) . PHP_EOL); + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php b/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php new file mode 100644 index 0000000..30d36eb --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console, + Doctrine\ORM\Tools\Console\MetadataFilter, + Doctrine\ORM\Tools\EntityRepositoryGenerator; + +/** + * Command to generate repository classes for mapping information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class GenerateRepositoriesCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:generate-repositories') + ->setDescription('Generate repository classes from your mapping information.') + ->setDefinition(array( + new InputOption( + 'filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'A string pattern used to match entities that should be processed.' + ), + new InputArgument( + 'dest-path', InputArgument::REQUIRED, 'The path to generate your repository classes.' + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + $metadatas = MetadataFilter::filter($metadatas, $input->getOption('filter')); + + // Process destination directory + $destPath = realpath($input->getArgument('dest-path')); + + if ( ! file_exists($destPath)) { + throw new \InvalidArgumentException( + sprintf("Entities destination directory '%s' does not exist.", $destPath) + ); + } else if ( ! is_writable($destPath)) { + throw new \InvalidArgumentException( + sprintf("Entities destination directory '%s' does not have write permissions.", $destPath) + ); + } + + if (count($metadatas)) { + $numRepositories = 0; + $generator = new EntityRepositoryGenerator(); + + foreach ($metadatas as $metadata) { + if ($metadata->customRepositoryClassName) { + $output->write( + sprintf('Processing repository "%s"', $metadata->customRepositoryClassName) . PHP_EOL + ); + + $generator->writeEntityRepositoryClass($metadata->customRepositoryClassName, $destPath); + + $numRepositories++; + } + } + + if ($numRepositories) { + // Outputting information message + $output->write(PHP_EOL . sprintf('Repository classes generated to "%s"', $destPath) . PHP_EOL); + } else { + $output->write('No Repository classes were found to be processed.' . PHP_EOL); + } + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php b/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php new file mode 100644 index 0000000..c794cb9 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/RunDqlCommand.php @@ -0,0 +1,124 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Command to execute DQL queries in a given EntityManager. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class RunDqlCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:run-dql') + ->setDescription('Executes arbitrary DQL directly from the command line.') + ->setDefinition(array( + new InputArgument('dql', InputArgument::REQUIRED, 'The DQL to execute.'), + new InputOption( + 'hydrate', null, InputOption::VALUE_REQUIRED, + 'Hydration mode of result set. Should be either: object, array, scalar or single-scalar.', + 'object' + ), + new InputOption( + 'first-result', null, InputOption::VALUE_REQUIRED, + 'The first result in the result set.' + ), + new InputOption( + 'max-result', null, InputOption::VALUE_REQUIRED, + 'The maximum number of results in the result set.' + ), + new InputOption( + 'depth', null, InputOption::VALUE_REQUIRED, + 'Dumping depth of Entity graph.', 7 + ) + )) + ->setHelp(<<getHelper('em')->getEntityManager(); + + if (($dql = $input->getArgument('dql')) === null) { + throw new \RuntimeException("Argument 'DQL' is required in order to execute this command correctly."); + } + + $depth = $input->getOption('depth'); + + if ( ! is_numeric($depth)) { + throw new \LogicException("Option 'depth' must contains an integer value"); + } + + $hydrationModeName = $input->getOption('hydrate'); + $hydrationMode = 'Doctrine\ORM\Query::HYDRATE_' . strtoupper(str_replace('-', '_', $hydrationModeName)); + + if ( ! defined($hydrationMode)) { + throw new \RuntimeException( + "Hydration mode '$hydrationModeName' does not exist. It should be either: object. array, scalar or single-scalar." + ); + } + + $query = $em->createQuery($dql); + + if (($firstResult = $input->getOption('first-result')) !== null) { + if ( ! is_numeric($firstResult)) { + throw new \LogicException("Option 'first-result' must contains an integer value"); + } + + $query->setFirstResult((int) $firstResult); + } + + if (($maxResult = $input->getOption('max-result')) !== null) { + if ( ! is_numeric($maxResult)) { + throw new \LogicException("Option 'max-result' must contains an integer value"); + } + + $query->setMaxResults((int) $maxResult); + } + + $resultSet = $query->execute(array(), constant($hydrationMode)); + + \Doctrine\Common\Util\Debug::dump($resultSet, $input->getOption('depth')); + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php b/Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php new file mode 100644 index 0000000..cd53948 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/SchemaTool/AbstractCommand.php @@ -0,0 +1,64 @@ +. +*/ + +namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Symfony\Component\Console\Command\Command, + Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper, + Doctrine\ORM\Tools\SchemaTool, + Doctrine\ORM\Mapping\Driver\AbstractFileDriver; + +abstract class AbstractCommand extends Command +{ + /** + * @param InputInterface $input + * @param OutputInterface $output + * @param SchemaTool $schemaTool + * @param array $metadatas + */ + abstract protected function executeSchemaCommand(InputInterface $input, OutputInterface $output, SchemaTool $schemaTool, array $metadatas); + + /** + * @see Console\Command\Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $emHelper = $this->getHelper('em'); + + /* @var $em \Doctrine\ORM\EntityManager */ + $em = $emHelper->getEntityManager(); + + $metadatas = $em->getMetadataFactory()->getAllMetadata(); + + if ( ! empty($metadatas)) { + // Create SchemaTool + $tool = new \Doctrine\ORM\Tools\SchemaTool($em); + + $this->executeSchemaCommand($input, $output, $tool, $metadatas); + } else { + $output->write('No Metadata Classes to process.' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php b/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php new file mode 100644 index 0000000..e18a9c5 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php @@ -0,0 +1,79 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Doctrine\ORM\Tools\SchemaTool; + +/** + * Command to create the database schema for a set of classes based on their mappings. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class CreateCommand extends AbstractCommand +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:schema-tool:create') + ->setDescription( + 'Processes the schema and either create it directly on EntityManager Storage Connection or generate the SQL output.' + ) + ->setDefinition(array( + new InputOption( + 'dump-sql', null, InputOption::VALUE_NONE, + 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' + ) + )) + ->setHelp(<<write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + + if ($input->getOption('dump-sql') === true) { + $sqls = $schemaTool->getCreateSchemaSql($metadatas); + $output->write(implode(';' . PHP_EOL, $sqls) . PHP_EOL); + } else { + $output->write('Creating database schema...' . PHP_EOL); + $schemaTool->createSchema($metadatas); + $output->write('Database schema created successfully!' . PHP_EOL); + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php new file mode 100644 index 0000000..82d91f4 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php @@ -0,0 +1,111 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Doctrine\ORM\Tools\SchemaTool; + +/** + * Command to drop the database schema for a set of classes based on their mappings. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DropCommand extends AbstractCommand +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:schema-tool:drop') + ->setDescription( + 'Drop the complete database schema of EntityManager Storage Connection or generate the corresponding SQL output.' + ) + ->setDefinition(array( + new InputOption( + 'dump-sql', null, InputOption::VALUE_NONE, + 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' + ), + new InputOption( + 'force', null, InputOption::VALUE_NONE, + "Don't ask for the deletion of the database, but force the operation to run." + ), + new InputOption( + 'full-database', null, InputOption::VALUE_NONE, + 'Instead of using the Class Metadata to detect the database table schema, drop ALL assets that the database contains.' + ), + )) + ->setHelp(<<getOption('full-database')); + + if ($input->getOption('dump-sql') === true) { + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + $output->write(implode(';' . PHP_EOL, $sqls) . PHP_EOL); + } else if ($input->getOption('force') === true) { + $output->write('Dropping database schema...' . PHP_EOL); + if ($isFullDatabaseDrop) { + $schemaTool->dropDatabase(); + } else { + $schemaTool->dropSchema($metadatas); + } + $output->write('Database schema dropped successfully!' . PHP_EOL); + } else { + $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL . PHP_EOL); + + if ($isFullDatabaseDrop) { + $sqls = $schemaTool->getDropDatabaseSQL(); + } else { + $sqls = $schemaTool->getDropSchemaSQL($metadatas); + } + + if (count($sqls)) { + $output->write('Schema-Tool would execute ' . count($sqls) . ' queries to drop the database.' . PHP_EOL); + $output->write('Please run the operation with --force to execute these queries or use --dump-sql to see them.' . PHP_EOL); + } else { + $output->write('Nothing to drop. The database is empty!' . PHP_EOL); + } + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php b/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php new file mode 100644 index 0000000..f1a3a73 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\SchemaTool; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console\Input\InputInterface, + Symfony\Component\Console\Output\OutputInterface, + Doctrine\ORM\Tools\SchemaTool; + +/** + * Command to update the database schema for a set of classes based on their mappings. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class UpdateCommand extends AbstractCommand +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:schema-tool:update') + ->setDescription( + 'Processes the schema and either update the database schema of EntityManager Storage Connection or generate the SQL output.' + ) + ->setDefinition(array( + new InputOption( + 'complete', null, InputOption::VALUE_NONE, + 'If defined, all assets of the database which are not relevant to the current metadata will be dropped.' + ), + new InputOption( + 'dump-sql', null, InputOption::VALUE_NONE, + 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' + ), + new InputOption( + 'force', null, InputOption::VALUE_NONE, + "Don't ask for the incremental update of the database, but force the operation to run." + ), + )) + ->setHelp(<<getOption('complete') !== true); + + if ($input->getOption('dump-sql') === true) { + $sqls = $schemaTool->getUpdateSchemaSql($metadatas, $saveMode); + $output->write(implode(';' . PHP_EOL, $sqls) . PHP_EOL); + } else if ($input->getOption('force') === true) { + $output->write('Updating database schema...' . PHP_EOL); + $schemaTool->updateSchema($metadatas, $saveMode); + $output->write('Database schema updated successfully!' . PHP_EOL); + } else { + $output->write('ATTENTION: This operation should not be executed in an production enviroment.' . PHP_EOL); + $output->write('Use the incremental update to detect changes during development and use' . PHP_EOL); + $output->write('this SQL DDL to manually update your database in production.' . PHP_EOL . PHP_EOL); + + $sqls = $schemaTool->getUpdateSchemaSql($metadatas, $saveMode); + + if (count($sqls)) { + $output->write('Schema-Tool would execute ' . count($sqls) . ' queries to update the database.' . PHP_EOL); + $output->write('Please run the operation with --force to execute these queries or use --dump-sql to see them.' . PHP_EOL); + } else { + $output->write('Nothing to update. The database is in sync with the current entity metadata.' . PHP_EOL); + } + } + } +} diff --git a/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php b/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php new file mode 100644 index 0000000..994b895 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php @@ -0,0 +1,89 @@ +. +*/ + +namespace Doctrine\ORM\Tools\Console\Command; + +use Symfony\Component\Console\Input\InputArgument, + Symfony\Component\Console\Input\InputOption, + Symfony\Component\Console; + +/** + * Validate that the current mapping is valid + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ValidateSchemaCommand extends Console\Command\Command +{ + /** + * @see Console\Command\Command + */ + protected function configure() + { + $this + ->setName('orm:validate-schema') + ->setDescription('Validate that the mapping files.') + ->setHelp(<<getHelper('em')->getEntityManager(); + + $validator = new \Doctrine\ORM\Tools\SchemaValidator($em); + $errors = $validator->validateMapping(); + + $exit = 0; + if ($errors) { + foreach ($errors AS $className => $errorMessages) { + $output->write("[Mapping] FAIL - The entity-class '" . $className . "' mapping is invalid:\n"); + foreach ($errorMessages AS $errorMessage) { + $output->write('* ' . $errorMessage . "\n"); + } + $output->write("\n"); + } + $exit += 1; + } else { + $output->write('[Mapping] OK - The mapping files are correct.' . "\n"); + } + + if (!$validator->schemaInSyncWithMetadata()) { + $output->write('[Database] FAIL - The database schema is not in sync with the current mapping file.' . "\n"); + $exit += 2; + } else { + $output->write('[Database] OK - The database schema is in sync with the mapping files.' . "\n"); + } + + exit($exit); + } +} diff --git a/Doctrine/ORM/Tools/Console/ConsoleRunner.php b/Doctrine/ORM/Tools/Console/ConsoleRunner.php new file mode 100644 index 0000000..f74713a --- /dev/null +++ b/Doctrine/ORM/Tools/Console/ConsoleRunner.php @@ -0,0 +1,69 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Helper\HelperSet; + +class ConsoleRunner +{ + /** + * Run console with the given helperset. + * + * @param \Symfony\Component\Console\Helper\HelperSet $helperSet + * @return void + */ + static public function run(HelperSet $helperSet) + { + $cli = new Application('Doctrine Command Line Interface', \Doctrine\ORM\Version::VERSION); + $cli->setCatchExceptions(true); + $cli->setHelperSet($helperSet); + self::addCommands($cli); + $cli->run(); + } + + /** + * @param Application $cli + */ + static public function addCommands(Application $cli) + { + $cli->addCommands(array( + // DBAL Commands + new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(), + new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), + + // ORM Commands + new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand(), + new \Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand(), + new \Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand(), + new \Doctrine\ORM\Tools\Console\Command\ConvertDoctrine1SchemaCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand(), + new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(), + new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(), + new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(), + new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand(), + )); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php b/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php new file mode 100644 index 0000000..c28622e --- /dev/null +++ b/Doctrine/ORM/Tools/Console/Helper/EntityManagerHelper.php @@ -0,0 +1,74 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Helper; + +use Symfony\Component\Console\Helper\Helper, + Doctrine\ORM\EntityManager; + +/** + * Doctrine CLI Connection Helper. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EntityManagerHelper extends Helper +{ + /** + * Doctrine ORM EntityManager + * @var EntityManager + */ + protected $_em; + + /** + * Constructor + * + * @param Connection $connection Doctrine Database Connection + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + } + + /** + * Retrieves Doctrine ORM EntityManager + * + * @return EntityManager + */ + public function getEntityManager() + { + return $this->_em; + } + + /** + * @see Helper + */ + public function getName() + { + return 'entityManager'; + } +} diff --git a/Doctrine/ORM/Tools/Console/MetadataFilter.php b/Doctrine/ORM/Tools/Console/MetadataFilter.php new file mode 100644 index 0000000..efd7208 --- /dev/null +++ b/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -0,0 +1,80 @@ +. +*/ + +namespace Doctrine\ORM\Tools\Console; + +/** + * Used by CLI Tools to restrict entity-based commands to given patterns. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class MetadataFilter extends \FilterIterator implements \Countable +{ + /** + * Filter Metadatas by one or more filter options. + * + * @param array $metadatas + * @param array|string $filter + * @return array + */ + static public function filter(array $metadatas, $filter) + { + $metadatas = new MetadataFilter(new \ArrayIterator($metadatas), $filter); + return iterator_to_array($metadatas); + } + + private $_filter = array(); + + public function __construct(\ArrayIterator $metadata, $filter) + { + $this->_filter = (array)$filter; + parent::__construct($metadata); + } + + public function accept() + { + if (count($this->_filter) == 0) { + return true; + } + + $it = $this->getInnerIterator(); + $metadata = $it->current(); + + foreach ($this->_filter AS $filter) { + if (strpos($metadata->name, $filter) !== false) { + return true; + } + } + return false; + } + + public function count() + { + return count($this->getInnerIterator()); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/ConvertDoctrine1Schema.php b/Doctrine/ORM/Tools/ConvertDoctrine1Schema.php new file mode 100644 index 0000000..14c9ada --- /dev/null +++ b/Doctrine/ORM/Tools/ConvertDoctrine1Schema.php @@ -0,0 +1,274 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Tools\Export\Driver\AbstractExporter, + Doctrine\Common\Util\Inflector; + +/** + * Class to help with converting Doctrine 1 schema files to Doctrine 2 mapping files + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class ConvertDoctrine1Schema +{ + private $_legacyTypeMap = array( + // TODO: This list may need to be updated + 'clob' => 'text', + 'timestamp' => 'datetime', + 'enum' => 'string' + ); + + /** + * Constructor passes the directory or array of directories + * to convert the Doctrine 1 schema files from + * + * @param array $from + * @author Jonathan Wage + */ + public function __construct($from) + { + $this->_from = (array) $from; + } + + /** + * Get an array of ClassMetadataInfo instances from the passed + * Doctrine 1 schema + * + * @return array $metadatas An array of ClassMetadataInfo instances + */ + public function getMetadata() + { + $schema = array(); + foreach ($this->_from as $path) { + if (is_dir($path)) { + $files = glob($path . '/*.yml'); + foreach ($files as $file) { + $schema = array_merge($schema, (array) \Symfony\Component\Yaml\Yaml::load($file)); + } + } else { + $schema = array_merge($schema, (array) \Symfony\Component\Yaml\Yaml::load($path)); + } + } + + $metadatas = array(); + foreach ($schema as $className => $mappingInformation) { + $metadatas[] = $this->_convertToClassMetadataInfo($className, $mappingInformation); + } + + return $metadatas; + } + + private function _convertToClassMetadataInfo($className, $mappingInformation) + { + $metadata = new ClassMetadataInfo($className); + + $this->_convertTableName($className, $mappingInformation, $metadata); + $this->_convertColumns($className, $mappingInformation, $metadata); + $this->_convertIndexes($className, $mappingInformation, $metadata); + $this->_convertRelations($className, $mappingInformation, $metadata); + + return $metadata; + } + + private function _convertTableName($className, array $model, ClassMetadataInfo $metadata) + { + if (isset($model['tableName']) && $model['tableName']) { + $e = explode('.', $model['tableName']); + if (count($e) > 1) { + $metadata->table['schema'] = $e[0]; + $metadata->table['name'] = $e[1]; + } else { + $metadata->table['name'] = $e[0]; + } + } + } + + private function _convertColumns($className, array $model, ClassMetadataInfo $metadata) + { + $id = false; + + if (isset($model['columns']) && $model['columns']) { + foreach ($model['columns'] as $name => $column) { + $fieldMapping = $this->_convertColumn($className, $name, $column, $metadata); + + if (isset($fieldMapping['id']) && $fieldMapping['id']) { + $id = true; + } + } + } + + if ( ! $id) { + $fieldMapping = array( + 'fieldName' => 'id', + 'columnName' => 'id', + 'type' => 'integer', + 'id' => true + ); + $metadata->mapField($fieldMapping); + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + } + } + + private function _convertColumn($className, $name, $column, ClassMetadataInfo $metadata) + { + if (is_string($column)) { + $string = $column; + $column = array(); + $column['type'] = $string; + } + if ( ! isset($column['name'])) { + $column['name'] = $name; + } + // check if a column alias was used (column_name as field_name) + if (preg_match("/(\w+)\sas\s(\w+)/i", $column['name'], $matches)) { + $name = $matches[1]; + $column['name'] = $name; + $column['alias'] = $matches[2]; + } + if (preg_match("/([a-zA-Z]+)\(([0-9]+)\)/", $column['type'], $matches)) { + $column['type'] = $matches[1]; + $column['length'] = $matches[2]; + } + $column['type'] = strtolower($column['type']); + // check if legacy column type (1.x) needs to be mapped to a 2.0 one + if (isset($this->_legacyTypeMap[$column['type']])) { + $column['type'] = $this->_legacyTypeMap[$column['type']]; + } + if ( ! \Doctrine\DBAL\Types\Type::hasType($column['type'])) { + throw ToolsException::couldNotMapDoctrine1Type($column['type']); + } + + $fieldMapping = array(); + if (isset($column['primary'])) { + $fieldMapping['id'] = true; + } + $fieldMapping['fieldName'] = isset($column['alias']) ? $column['alias'] : $name; + $fieldMapping['columnName'] = $column['name']; + $fieldMapping['type'] = $column['type']; + if (isset($column['length'])) { + $fieldMapping['length'] = $column['length']; + } + $allowed = array('precision', 'scale', 'unique', 'options', 'notnull', 'version'); + foreach ($column as $key => $value) { + if (in_array($key, $allowed)) { + $fieldMapping[$key] = $value; + } + } + + $metadata->mapField($fieldMapping); + + if (isset($column['autoincrement'])) { + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + } else if (isset($column['sequence'])) { + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE); + $metadata->setSequenceGeneratorDefinition($definition); + $definition = array( + 'sequenceName' => is_array($column['sequence']) ? $column['sequence']['name']:$column['sequence'] + ); + if (isset($column['sequence']['size'])) { + $definition['allocationSize'] = $column['sequence']['size']; + } + if (isset($column['sequence']['value'])) { + $definition['initialValue'] = $column['sequence']['value']; + } + } + return $fieldMapping; + } + + private function _convertIndexes($className, array $model, ClassMetadataInfo $metadata) + { + if (isset($model['indexes']) && $model['indexes']) { + foreach ($model['indexes'] as $name => $index) { + $type = (isset($index['type']) && $index['type'] == 'unique') + ? 'uniqueConstraints' : 'indexes'; + + $metadata->table[$type][$name] = array( + 'columns' => $index['fields'] + ); + } + } + } + + private function _convertRelations($className, array $model, ClassMetadataInfo $metadata) + { + if (isset($model['relations']) && $model['relations']) { + foreach ($model['relations'] as $name => $relation) { + if ( ! isset($relation['alias'])) { + $relation['alias'] = $name; + } + if ( ! isset($relation['class'])) { + $relation['class'] = $name; + } + if ( ! isset($relation['local'])) { + $relation['local'] = Inflector::tableize($relation['class']); + } + if ( ! isset($relation['foreign'])) { + $relation['foreign'] = 'id'; + } + if ( ! isset($relation['foreignAlias'])) { + $relation['foreignAlias'] = $className; + } + + if (isset($relation['refClass'])) { + $type = 'many'; + $foreignType = 'many'; + $joinColumns = array(); + } else { + $type = isset($relation['type']) ? $relation['type'] : 'one'; + $foreignType = isset($relation['foreignType']) ? $relation['foreignType'] : 'many'; + $joinColumns = array( + array( + 'name' => $relation['local'], + 'referencedColumnName' => $relation['foreign'], + 'onDelete' => isset($relation['onDelete']) ? $relation['onDelete'] : null, + 'onUpdate' => isset($relation['onUpdate']) ? $relation['onUpdate'] : null, + ) + ); + } + + if ($type == 'one' && $foreignType == 'one') { + $method = 'mapOneToOne'; + } else if ($type == 'many' && $foreignType == 'many') { + $method = 'mapManyToMany'; + } else { + $method = 'mapOneToMany'; + } + + $associationMapping = array(); + $associationMapping['fieldName'] = $relation['alias']; + $associationMapping['targetEntity'] = $relation['class']; + $associationMapping['mappedBy'] = $relation['foreignAlias']; + $associationMapping['joinColumns'] = $joinColumns; + + $metadata->$method($associationMapping); + } + } + } +} diff --git a/Doctrine/ORM/Tools/DisconnectedClassMetadataFactory.php b/Doctrine/ORM/Tools/DisconnectedClassMetadataFactory.php new file mode 100644 index 0000000..55503d4 --- /dev/null +++ b/Doctrine/ORM/Tools/DisconnectedClassMetadataFactory.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Mapping\ClassMetadataFactory; +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * The DisconnectedClassMetadataFactory is used to create ClassMetadataInfo objects + * that do not require the entity class actually exist. This allows us to + * load some mapping information and use it to do things like generate code + * from the mapping information. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class DisconnectedClassMetadataFactory extends ClassMetadataFactory +{ + /** + * @override + */ + protected function newClassMetadataInstance($className) + { + $metadata = new ClassMetadataInfo($className); + if (strpos($className, "\\") !== false) { + $metadata->namespace = strrev(substr( strrev($className), strpos(strrev($className), "\\")+1 )); + } else { + $metadata->namespace = ""; + } + return $metadata; + } + + /** + * @override + */ + protected function getParentClasses($name) + { + return array(); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/EntityGenerator.php b/Doctrine/ORM/Tools/EntityGenerator.php new file mode 100644 index 0000000..b0c8320 --- /dev/null +++ b/Doctrine/ORM/Tools/EntityGenerator.php @@ -0,0 +1,944 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\AssociationMapping, + Doctrine\Common\Util\Inflector; + +/** + * Generic class used to generate PHP5 entity classes from ClassMetadataInfo instances + * + * [php] + * $classes = $em->getClassMetadataFactory()->getAllMetadata(); + * + * $generator = new \Doctrine\ORM\Tools\EntityGenerator(); + * $generator->setGenerateAnnotations(true); + * $generator->setGenerateStubMethods(true); + * $generator->setRegenerateEntityIfExists(false); + * $generator->setUpdateEntityIfExists(true); + * $generator->generate($classes, '/path/to/generate/entities'); + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EntityGenerator +{ + /** The extension to use for written php files */ + private $_extension = '.php'; + + /** Whether or not the current ClassMetadataInfo instance is new or old */ + private $_isNew = true; + + /** If isNew is false then this variable contains instance of ReflectionClass for current entity */ + private $_reflection; + + /** Number of spaces to use for indention in generated code */ + private $_numSpaces = 4; + + /** The actual spaces to use for indention */ + private $_spaces = ' '; + + /** The class all generated entities should extend */ + private $_classToExtend; + + /** Whether or not to generation annotations */ + private $_generateAnnotations = false; + + /** Whether or not to generated sub methods */ + private $_generateEntityStubMethods = false; + + /** Whether or not to update the entity class if it exists already */ + private $_updateEntityIfExists = false; + + /** Whether or not to re-generate entity class if it exists already */ + private $_regenerateEntityIfExists = false; + + private static $_classTemplate = +' + + + +{ + +}'; + + private static $_getMethodTemplate = +'/** + * + * + * @return $ + */ +public function () +{ +return $this->; +}'; + + private static $_setMethodTemplate = +'/** + * + * + * @param $ + */ +public function ($) +{ +$this-> = $; +}'; + + private static $_addMethodTemplate = +'/** + * + * + * @param $ + */ +public function ($) +{ +$this->[] = $; +}'; + + private static $_lifecycleCallbackMethodTemplate = +'/** + * @ + */ +public function () +{ +// Add your code here +}'; + + /** + * Generate and write entity classes for the given array of ClassMetadataInfo instances + * + * @param array $metadatas + * @param string $outputDirectory + * @return void + */ + public function generate(array $metadatas, $outputDirectory) + { + foreach ($metadatas as $metadata) { + $this->writeEntityClass($metadata, $outputDirectory); + } + } + + /** + * Generated and write entity class to disk for the given ClassMetadataInfo instance + * + * @param ClassMetadataInfo $metadata + * @param string $outputDirectory + * @return void + */ + public function writeEntityClass(ClassMetadataInfo $metadata, $outputDirectory) + { + $path = $outputDirectory . '/' . str_replace('\\', DIRECTORY_SEPARATOR, $metadata->name) . $this->_extension; + $dir = dirname($path); + + if ( ! is_dir($dir)) { + mkdir($dir, 0777, true); + } + + $this->_isNew = ! file_exists($path); + + if ( ! $this->_isNew) { + require_once $path; + + $this->_reflection = new \ReflectionClass($metadata->name); + } + + // If entity doesn't exist or we're re-generating the entities entirely + if ($this->_isNew || ( ! $this->_isNew && $this->_regenerateEntityIfExists)) { + file_put_contents($path, $this->generateEntityClass($metadata)); + // If entity exists and we're allowed to update the entity class + } else if ( ! $this->_isNew && $this->_updateEntityIfExists) { + file_put_contents($path, $this->generateUpdatedEntityClass($metadata, $path)); + } + } + + /** + * Generate a PHP5 Doctrine 2 entity class from the given ClassMetadataInfo instance + * + * @param ClassMetadataInfo $metadata + * @return string $code + */ + public function generateEntityClass(ClassMetadataInfo $metadata) + { + $placeHolders = array( + '', + '', + '', + '' + ); + + $replacements = array( + $this->_generateEntityNamespace($metadata), + $this->_generateEntityDocBlock($metadata), + $this->_generateEntityClassName($metadata), + $this->_generateEntityBody($metadata) + ); + + $code = str_replace($placeHolders, $replacements, self::$_classTemplate); + return str_replace('', $this->_spaces, $code); + } + + /** + * Generate the updated code for the given ClassMetadataInfo and entity at path + * + * @param ClassMetadataInfo $metadata + * @param string $path + * @return string $code; + */ + public function generateUpdatedEntityClass(ClassMetadataInfo $metadata, $path) + { + $currentCode = file_get_contents($path); + + $body = $this->_generateEntityBody($metadata); + $body = str_replace('', $this->_spaces, $body); + $last = strrpos($currentCode, '}'); + + return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : ''). "}"; + } + + /** + * Set the number of spaces the exported class should have + * + * @param integer $numSpaces + * @return void + */ + public function setNumSpaces($numSpaces) + { + $this->_spaces = str_repeat(' ', $numSpaces); + $this->_numSpaces = $numSpaces; + } + + /** + * Set the extension to use when writing php files to disk + * + * @param string $extension + * @return void + */ + public function setExtension($extension) + { + $this->_extension = $extension; + } + + /** + * Set the name of the class the generated classes should extend from + * + * @return void + */ + public function setClassToExtend($classToExtend) + { + $this->_classToExtend = $classToExtend; + } + + /** + * Set whether or not to generate annotations for the entity + * + * @param bool $bool + * @return void + */ + public function setGenerateAnnotations($bool) + { + $this->_generateAnnotations = $bool; + } + + /** + * Set whether or not to try and update the entity if it already exists + * + * @param bool $bool + * @return void + */ + public function setUpdateEntityIfExists($bool) + { + $this->_updateEntityIfExists = $bool; + } + + /** + * Set whether or not to regenerate the entity if it exists + * + * @param bool $bool + * @return void + */ + public function setRegenerateEntityIfExists($bool) + { + $this->_regenerateEntityIfExists = $bool; + } + + /** + * Set whether or not to generate stub methods for the entity + * + * @param bool $bool + * @return void + */ + public function setGenerateStubMethods($bool) + { + $this->_generateEntityStubMethods = $bool; + } + + private function _generateEntityNamespace(ClassMetadataInfo $metadata) + { + if ($this->_hasNamespace($metadata)) { + return 'namespace ' . $this->_getNamespace($metadata) .';'; + } + } + + private function _generateEntityClassName(ClassMetadataInfo $metadata) + { + return 'class ' . $this->_getClassName($metadata) . + ($this->_extendsClass() ? ' extends ' . $this->_getClassToExtendName() : null); + } + + private function _generateEntityBody(ClassMetadataInfo $metadata) + { + $fieldMappingProperties = $this->_generateEntityFieldMappingProperties($metadata); + $associationMappingProperties = $this->_generateEntityAssociationMappingProperties($metadata); + $stubMethods = $this->_generateEntityStubMethods ? $this->_generateEntityStubMethods($metadata) : null; + $lifecycleCallbackMethods = $this->_generateEntityLifecycleCallbackMethods($metadata); + + $code = array(); + + if ($fieldMappingProperties) { + $code[] = $fieldMappingProperties; + } + + if ($associationMappingProperties) { + $code[] = $associationMappingProperties; + } + + if ($stubMethods) { + $code[] = $stubMethods; + } + + if ($lifecycleCallbackMethods) { + $code[] = $lifecycleCallbackMethods; + } + + return implode("\n", $code); + } + + private function _hasProperty($property, ClassMetadataInfo $metadata) + { + return ($this->_isNew) ? false : $this->_reflection->hasProperty($property); + } + + private function _hasMethod($method, ClassMetadataInfo $metadata) + { + return ($this->_isNew) ? false : $this->_reflection->hasMethod($method); + } + + private function _hasNamespace(ClassMetadataInfo $metadata) + { + return strpos($metadata->name, '\\') ? true : false; + } + + private function _extendsClass() + { + return $this->_classToExtend ? true : false; + } + + private function _getClassToExtend() + { + return $this->_classToExtend; + } + + private function _getClassToExtendName() + { + $refl = new \ReflectionClass($this->_getClassToExtend()); + + return '\\' . $refl->getName(); + } + + private function _getClassName(ClassMetadataInfo $metadata) + { + return ($pos = strrpos($metadata->name, '\\')) + ? substr($metadata->name, $pos + 1, strlen($metadata->name)) : $metadata->name; + } + + private function _getNamespace(ClassMetadataInfo $metadata) + { + return substr($metadata->name, 0, strrpos($metadata->name, '\\')); + } + + private function _generateEntityDocBlock(ClassMetadataInfo $metadata) + { + $lines = array(); + $lines[] = '/**'; + $lines[] = ' * '.$metadata->name; + + if ($this->_generateAnnotations) { + $lines[] = ' *'; + + $methods = array( + '_generateTableAnnotation', + '_generateInheritanceAnnotation', + '_generateDiscriminatorColumnAnnotation', + '_generateDiscriminatorMapAnnotation' + ); + + foreach ($methods as $method) { + if ($code = $this->$method($metadata)) { + $lines[] = ' * ' . $code; + } + } + + if ($metadata->isMappedSuperclass) { + $lines[] = ' * @MappedSupperClass'; + } else { + $lines[] = ' * @Entity'; + } + + if ($metadata->customRepositoryClassName) { + $lines[count($lines) - 1] .= '(repositoryClass="' . $metadata->customRepositoryClassName . '")'; + } + + if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) { + $lines[] = ' * @HasLifecycleCallbacks'; + } + } + + $lines[] = ' */'; + + return implode("\n", $lines); + } + + private function _generateTableAnnotation($metadata) + { + $table = array(); + if ($metadata->table['name']) { + $table[] = 'name="' . $metadata->table['name'] . '"'; + } + + if (isset($metadata->table['schema'])) { + $table[] = 'schema="' . $metadata->table['schema'] . '"'; + } + + return '@Table(' . implode(', ', $table) . ')'; + } + + private function _generateInheritanceAnnotation($metadata) + { + if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) { + return '@InheritanceType("'.$this->_getInheritanceTypeString($metadata->inheritanceType).'")'; + } + } + + private function _generateDiscriminatorColumnAnnotation($metadata) + { + if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) { + $discrColumn = $metadata->discriminatorValue; + $columnDefinition = 'name="' . $discrColumn['name'] + . '", type="' . $discrColumn['type'] + . '", length=' . $discrColumn['length']; + + return '@DiscriminatorColumn(' . $columnDefinition . ')'; + } + } + + private function _generateDiscriminatorMapAnnotation($metadata) + { + if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) { + $inheritanceClassMap = array(); + + foreach ($metadata->discriminatorMap as $type => $class) { + $inheritanceClassMap[] .= '"' . $type . '" = "' . $class . '"'; + } + + return '@DiscriminatorMap({' . implode(', ', $inheritanceClassMap) . '})'; + } + } + + private function _generateEntityStubMethods(ClassMetadataInfo $metadata) + { + $methods = array(); + + foreach ($metadata->fieldMappings as $fieldMapping) { + if ( ! isset($fieldMapping['id']) || ! $fieldMapping['id']) { + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $fieldMapping['fieldName'], $fieldMapping['type'])) { + $methods[] = $code; + } + } + + if ($code = $this->_generateEntityStubMethod($metadata, 'get', $fieldMapping['fieldName'], $fieldMapping['type'])) { + $methods[] = $code; + } + } + + foreach ($metadata->associationMappings as $associationMapping) { + if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + } else if ($associationMapping['type'] == ClassMetadataInfo::ONE_TO_MANY) { + if ($associationMapping['isOwningSide']) { + if ($code = $this->_generateEntityStubMethod($metadata, 'set', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + } else { + if ($code = $this->_generateEntityStubMethod($metadata, 'add', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], 'Doctrine\Common\Collections\Collection')) { + $methods[] = $code; + } + } + } else if ($associationMapping['type'] == ClassMetadataInfo::MANY_TO_MANY) { + if ($code = $this->_generateEntityStubMethod($metadata, 'add', $associationMapping['fieldName'], $associationMapping['targetEntity'])) { + $methods[] = $code; + } + if ($code = $this->_generateEntityStubMethod($metadata, 'get', $associationMapping['fieldName'], 'Doctrine\Common\Collections\Collection')) { + $methods[] = $code; + } + } + } + + return implode("\n\n", $methods); + } + + private function _generateEntityLifecycleCallbackMethods(ClassMetadataInfo $metadata) + { + if (isset($metadata->lifecycleCallbacks) && $metadata->lifecycleCallbacks) { + $methods = array(); + + foreach ($metadata->lifecycleCallbacks as $name => $callbacks) { + foreach ($callbacks as $callback) { + if ($code = $this->_generateLifecycleCallbackMethod($name, $callback, $metadata)) { + $methods[] = $code; + } + } + } + + return implode('', $methods); + } + } + + private function _generateEntityAssociationMappingProperties(ClassMetadataInfo $metadata) + { + $lines = array(); + + foreach ($metadata->associationMappings as $associationMapping) { + if ($this->_hasProperty($associationMapping['fieldName'], $metadata)) { + continue; + } + + $lines[] = $this->_generateAssociationMappingPropertyDocBlock($associationMapping, $metadata); + $lines[] = $this->_spaces . 'private $' . $associationMapping['fieldName'] + . ($associationMapping['type'] == 'manyToMany' ? ' = array()' : null) . ";\n"; + } + + return implode("\n", $lines); + } + + private function _generateEntityFieldMappingProperties(ClassMetadataInfo $metadata) + { + $lines = array(); + + foreach ($metadata->fieldMappings as $fieldMapping) { + if ($this->_hasProperty($fieldMapping['fieldName'], $metadata)) { + continue; + } + + $lines[] = $this->_generateFieldMappingPropertyDocBlock($fieldMapping, $metadata); + $lines[] = $this->_spaces . 'private $' . $fieldMapping['fieldName'] + . (isset($fieldMapping['default']) ? ' = ' . var_export($fieldMapping['default'], true) : null) . ";\n"; + } + + return implode("\n", $lines); + } + + private function _generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $fieldName, $typeHint = null) + { + $methodName = $type . Inflector::classify($fieldName); + + if ($this->_hasMethod($methodName, $metadata)) { + return; + } + + $var = sprintf('_%sMethodTemplate', $type); + $template = self::$$var; + + $variableType = $typeHint ? $typeHint . ' ' : null; + + $types = \Doctrine\DBAL\Types\Type::getTypesMap(); + $methodTypeHint = $typeHint && ! isset($types[$typeHint]) ? '\\' . $typeHint . ' ' : null; + + $replacements = array( + '' => ucfirst($type) . ' ' . $fieldName, + '' => $methodTypeHint, + '' => $variableType, + '' => Inflector::camelize($fieldName), + '' => $methodName, + '' => $fieldName + ); + + $method = str_replace( + array_keys($replacements), + array_values($replacements), + $template + ); + + return $this->_prefixCodeWithSpaces($method); + } + + private function _generateLifecycleCallbackMethod($name, $methodName, $metadata) + { + if ($this->_hasMethod($methodName, $metadata)) { + return; + } + + $replacements = array( + '' => $name, + '' => $methodName, + ); + + $method = str_replace( + array_keys($replacements), + array_values($replacements), + self::$_lifecycleCallbackMethodTemplate + ); + + return $this->_prefixCodeWithSpaces($method); + } + + private function _generateJoinColumnAnnotation(array $joinColumn) + { + $joinColumnAnnot = array(); + + if (isset($joinColumn['name'])) { + $joinColumnAnnot[] = 'name="' . $joinColumn['name'] . '"'; + } + + if (isset($joinColumn['referencedColumnName'])) { + $joinColumnAnnot[] = 'referencedColumnName="' . $joinColumn['referencedColumnName'] . '"'; + } + + if (isset($joinColumn['unique']) && $joinColumn['unique']) { + $joinColumnAnnot[] = 'unique=' . ($joinColumn['unique'] ? 'true' : 'false'); + } + + if (isset($joinColumn['nullable'])) { + $joinColumnAnnot[] = 'nullable=' . ($joinColumn['nullable'] ? 'true' : 'false'); + } + + if (isset($joinColumn['onDelete'])) { + $joinColumnAnnot[] = 'onDelete=' . ($joinColumn['onDelete'] ? 'true' : 'false'); + } + + if (isset($joinColumn['onUpdate'])) { + $joinColumnAnnot[] = 'onUpdate=' . ($joinColumn['onUpdate'] ? 'true' : 'false'); + } + + if (isset($joinColumn['columnDefinition'])) { + $joinColumnAnnot[] = 'columnDefinition="' . $joinColumn['columnDefinition'] . '"'; + } + + return '@JoinColumn(' . implode(', ', $joinColumnAnnot) . ')'; + } + + private function _generateAssociationMappingPropertyDocBlock(array $associationMapping, ClassMetadataInfo $metadata) + { + $lines = array(); + $lines[] = $this->_spaces . '/**'; + $lines[] = $this->_spaces . ' * @var ' . $associationMapping['targetEntity']; + + if ($this->_generateAnnotations) { + $lines[] = $this->_spaces . ' *'; + + $type = null; + switch ($associationMapping['type']) { + case ClassMetadataInfo::ONE_TO_ONE: + $type = 'OneToOne'; + break; + case ClassMetadataInfo::MANY_TO_ONE: + $type = 'ManyToOne'; + break; + case ClassMetadataInfo::ONE_TO_MANY: + $type = 'OneToMany'; + break; + case ClassMetadataInfo::MANY_TO_MANY: + $type = 'ManyToMany'; + break; + } + $typeOptions = array(); + + if (isset($associationMapping['targetEntity'])) { + $typeOptions[] = 'targetEntity="' . $associationMapping['targetEntity'] . '"'; + } + + if (isset($associationMapping['inversedBy'])) { + $typeOptions[] = 'inversedBy="' . $associationMapping['inversedBy'] . '"'; + } + + if (isset($associationMapping['mappedBy'])) { + $typeOptions[] = 'mappedBy="' . $associationMapping['mappedBy'] . '"'; + } + + if ($associationMapping['cascade']) { + $cascades = array(); + + if ($associationMapping['isCascadePersist']) $cascades[] = '"persist"'; + if ($associationMapping['isCascadeRemove']) $cascades[] = '"remove"'; + if ($associationMapping['isCascadeDetach']) $cascades[] = '"detach"'; + if ($associationMapping['isCascadeMerge']) $cascades[] = '"merge"'; + if ($associationMapping['isCascadeRefresh']) $cascades[] = '"refresh"'; + + $typeOptions[] = 'cascade={' . implode(',', $cascades) . '}'; + } + + if (isset($associationMapping['orphanRemoval']) && $associationMapping['orphanRemoval']) { + $typeOptions[] = 'orphanRemoval=' . ($associationMapping['orphanRemoval'] ? 'true' : 'false'); + } + + $lines[] = $this->_spaces . ' * @' . $type . '(' . implode(', ', $typeOptions) . ')'; + + if (isset($associationMapping['joinColumns']) && $associationMapping['joinColumns']) { + $lines[] = $this->_spaces . ' * @JoinColumns({'; + + $joinColumnsLines = array(); + + foreach ($associationMapping['joinColumns'] as $joinColumn) { + if ($joinColumnAnnot = $this->_generateJoinColumnAnnotation($joinColumn)) { + $joinColumnsLines[] = $this->_spaces . ' * ' . $joinColumnAnnot; + } + } + + $lines[] = implode(",\n", $joinColumnsLines); + $lines[] = $this->_spaces . ' * })'; + } + + if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) { + $joinTable = array(); + $joinTable[] = 'name="' . $associationMapping['joinTable']['name'] . '"'; + + if (isset($associationMapping['joinTable']['schema'])) { + $joinTable[] = 'schema="' . $associationMapping['joinTable']['schema'] . '"'; + } + + $lines[] = $this->_spaces . ' * @JoinTable(' . implode(', ', $joinTable) . ','; + $lines[] = $this->_spaces . ' * joinColumns={'; + + foreach ($associationMapping['joinTable']['joinColumns'] as $joinColumn) { + $lines[] = $this->_spaces . ' * ' . $this->_generateJoinColumnAnnotation($joinColumn); + } + + $lines[] = $this->_spaces . ' * },'; + $lines[] = $this->_spaces . ' * inverseJoinColumns={'; + + foreach ($associationMapping['joinTable']['inverseJoinColumns'] as $joinColumn) { + $lines[] = $this->_spaces . ' * ' . $this->_generateJoinColumnAnnotation($joinColumn); + } + + $lines[] = $this->_spaces . ' * }'; + $lines[] = $this->_spaces . ' * )'; + } + + if (isset($associationMapping['orderBy'])) { + $lines[] = $this->_spaces . ' * @OrderBy({'; + + foreach ($associationMapping['orderBy'] as $name => $direction) { + $lines[] = $this->_spaces . ' * "' . $name . '"="' . $direction . '",'; + } + + $lines[count($lines) - 1] = substr($lines[count($lines) - 1], 0, strlen($lines[count($lines) - 1]) - 1); + $lines[] = $this->_spaces . ' * })'; + } + } + + $lines[] = $this->_spaces . ' */'; + + return implode("\n", $lines); + } + + private function _generateFieldMappingPropertyDocBlock(array $fieldMapping, ClassMetadataInfo $metadata) + { + $lines = array(); + $lines[] = $this->_spaces . '/**'; + $lines[] = $this->_spaces . ' * @var ' . $fieldMapping['type'] . ' $' . $fieldMapping['fieldName']; + + if ($this->_generateAnnotations) { + $lines[] = $this->_spaces . ' *'; + + $column = array(); + if (isset($fieldMapping['columnName'])) { + $column[] = 'name="' . $fieldMapping['columnName'] . '"'; + } + + if (isset($fieldMapping['type'])) { + $column[] = 'type="' . $fieldMapping['type'] . '"'; + } + + if (isset($fieldMapping['length'])) { + $column[] = 'length=' . $fieldMapping['length']; + } + + if (isset($fieldMapping['precision'])) { + $column[] = 'precision=' . $fieldMapping['precision']; + } + + if (isset($fieldMapping['scale'])) { + $column[] = 'scale=' . $fieldMapping['scale']; + } + + if (isset($fieldMapping['nullable'])) { + $column[] = 'nullable=' . var_export($fieldMapping['nullable'], true); + } + + if (isset($fieldMapping['columnDefinition'])) { + $column[] = 'columnDefinition="' . $fieldMapping['columnDefinition'] . '"'; + } + + if (isset($fieldMapping['options'])) { + $options = array(); + + foreach ($fieldMapping['options'] as $key => $value) { + $value = var_export($value, true); + $value = str_replace("'", '"', $value); + $options[] = ! is_numeric($key) ? $key . '=' . $value:$value; + } + + if ($options) { + $column[] = 'options={' . implode(', ', $options) . '}'; + } + } + + if (isset($fieldMapping['unique'])) { + $column[] = 'unique=' . var_export($fieldMapping['unique'], true); + } + + $lines[] = $this->_spaces . ' * @Column(' . implode(', ', $column) . ')'; + + if (isset($fieldMapping['id']) && $fieldMapping['id']) { + $lines[] = $this->_spaces . ' * @Id'; + + if ($generatorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { + $lines[] = $this->_spaces.' * @GeneratedValue(strategy="' . $generatorType . '")'; + } + + if ($metadata->sequenceGeneratorDefinition) { + $sequenceGenerator = array(); + + if (isset($metadata->sequenceGeneratorDefinition['sequenceName'])) { + $sequenceGenerator[] = 'sequenceName="' . $metadata->sequenceGeneratorDefinition['sequenceName'] . '"'; + } + + if (isset($metadata->sequenceGeneratorDefinition['allocationSize'])) { + $sequenceGenerator[] = 'allocationSize="' . $metadata->sequenceGeneratorDefinition['allocationSize'] . '"'; + } + + if (isset($metadata->sequenceGeneratorDefinition['initialValue'])) { + $sequenceGenerator[] = 'initialValue="' . $metadata->sequenceGeneratorDefinition['initialValue'] . '"'; + } + + $lines[] = $this->_spaces . ' * @SequenceGenerator(' . implode(', ', $sequenceGenerator) . ')'; + } + } + + if (isset($fieldMapping['version']) && $fieldMapping['version']) { + $lines[] = $this->_spaces . ' * @Version'; + } + } + + $lines[] = $this->_spaces . ' */'; + + return implode("\n", $lines); + } + + private function _prefixCodeWithSpaces($code, $num = 1) + { + $lines = explode("\n", $code); + + foreach ($lines as $key => $value) { + $lines[$key] = str_repeat($this->_spaces, $num) . $lines[$key]; + } + + return implode("\n", $lines); + } + + private function _getInheritanceTypeString($type) + { + switch ($type) { + case ClassMetadataInfo::INHERITANCE_TYPE_NONE: + return 'NONE'; + + case ClassMetadataInfo::INHERITANCE_TYPE_JOINED: + return 'JOINED'; + + case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE: + return 'SINGLE_TABLE'; + + case ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS: + return 'PER_CLASS'; + + default: + throw new \InvalidArgumentException('Invalid provided InheritanceType: ' . $type); + } + } + + private function _getChangeTrackingPolicyString($policy) + { + switch ($policy) { + case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT: + return 'DEFERRED_IMPLICIT'; + + case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT: + return 'DEFERRED_EXPLICIT'; + + case ClassMetadataInfo::CHANGETRACKING_NOTIFY: + return 'NOTIFY'; + + default: + throw new \InvalidArgumentException('Invalid provided ChangeTrackingPolicy: ' . $policy); + } + } + + private function _getIdGeneratorTypeString($type) + { + switch ($type) { + case ClassMetadataInfo::GENERATOR_TYPE_AUTO: + return 'AUTO'; + + case ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE: + return 'SEQUENCE'; + + case ClassMetadataInfo::GENERATOR_TYPE_TABLE: + return 'TABLE'; + + case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY: + return 'IDENTITY'; + + case ClassMetadataInfo::GENERATOR_TYPE_NONE: + return 'NONE'; + + default: + throw new \InvalidArgumentException('Invalid provided IdGeneratorType: ' . $type); + } + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/EntityRepositoryGenerator.php b/Doctrine/ORM/Tools/EntityRepositoryGenerator.php new file mode 100644 index 0000000..74740db --- /dev/null +++ b/Doctrine/ORM/Tools/EntityRepositoryGenerator.php @@ -0,0 +1,83 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +/** + * Class to generate entity repository classes + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class EntityRepositoryGenerator +{ + protected static $_template = +'; + +use Doctrine\ORM\EntityRepository; + +/** + * + * + * This class was generated by the Doctrine ORM. Add your own custom + * repository methods below. + */ +class extends EntityRepository +{ +}'; + + public function generateEntityRepositoryClass($fullClassName) + { + $namespace = substr($fullClassName, 0, strrpos($fullClassName, '\\')); + $className = substr($fullClassName, strrpos($fullClassName, '\\') + 1, strlen($fullClassName)); + + $variables = array( + '' => $namespace, + '' => $className + ); + return str_replace(array_keys($variables), array_values($variables), self::$_template); + } + + public function writeEntityRepositoryClass($fullClassName, $outputDirectory) + { + $code = $this->generateEntityRepositoryClass($fullClassName); + + $path = $outputDirectory . DIRECTORY_SEPARATOR + . str_replace('\\', \DIRECTORY_SEPARATOR, $fullClassName) . '.php'; + $dir = dirname($path); + + if ( ! is_dir($dir)) { + mkdir($dir, 0777, true); + } + + if ( ! file_exists($path)) { + file_put_contents($path, $code); + } + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php b/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php new file mode 100644 index 0000000..02d04a2 --- /dev/null +++ b/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php @@ -0,0 +1,65 @@ +. +*/ + +namespace Doctrine\ORM\Tools\Event; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\ORM\EntityManager; + +/** + * Event Args used for the Events::postGenerateSchema event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class GenerateSchemaEventArgs extends \Doctrine\Common\EventArgs +{ + private $_em = null; + private $_schema = null; + + /** + * @param ClassMetadata $classMetadata + * @param Schema $schema + * @param Table $classTable + */ + public function __construct(EntityManager $em, Schema $schema) + { + $this->_em = $em; + $this->_schema = $schema; + } + + /** + * @return EntityManager + */ + public function getEntityManager() { + return $this->_em; + } + + /** + * @return Schema + */ + public function getSchema() { + return $this->_schema; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php b/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php new file mode 100644 index 0000000..54aa6e7 --- /dev/null +++ b/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php @@ -0,0 +1,75 @@ +. +*/ + +namespace Doctrine\ORM\Tools\Event; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; + +/** + * Event Args used for the Events::postGenerateSchemaTable event. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + */ +class GenerateSchemaTableEventArgs extends \Doctrine\Common\EventArgs +{ + private $_classMetadata = null; + private $_schema = null; + private $_classTable = null; + + /** + * @param ClassMetadata $classMetadata + * @param Schema $schema + * @param Table $classTable + */ + public function __construct(ClassMetadata $classMetadata, Schema $schema, Table $classTable) + { + $this->_classMetadata = $classMetadata; + $this->_schema = $schema; + $this->_classTable = $classTable; + } + + /** + * @return ClassMetadata + */ + public function getClassMetadata() { + return $this->_classMetadata; + } + + /** + * @return Schema + */ + public function getSchema() { + return $this->_schema; + } + + /** + * @return Table + */ + public function getClassTable() { + return $this->_classTable; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/ClassMetadataExporter.php b/Doctrine/ORM/Tools/Export/ClassMetadataExporter.php new file mode 100644 index 0000000..2b528a2 --- /dev/null +++ b/Doctrine/ORM/Tools/Export/ClassMetadataExporter.php @@ -0,0 +1,76 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export; + +use Doctrine\ORM\Tools\Export\ExportException, + Doctrine\ORM\EntityManager; + +/** + * Class used for converting your mapping information between the + * supported formats: yaml, xml, and php/annotation. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +class ClassMetadataExporter +{ + private static $_exporterDrivers = array( + 'xml' => 'Doctrine\ORM\Tools\Export\Driver\XmlExporter', + 'yaml' => 'Doctrine\ORM\Tools\Export\Driver\YamlExporter', + 'yml' => 'Doctrine\ORM\Tools\Export\Driver\YamlExporter', + 'php' => 'Doctrine\ORM\Tools\Export\Driver\PhpExporter', + 'annotation' => 'Doctrine\ORM\Tools\Export\Driver\AnnotationExporter' + ); + + /** + * Register a new exporter driver class under a specified name + * + * @param string $name + * @param string $class + */ + public static function registerExportDriver($name, $class) + { + self::$_exporterDrivers[$name] = $class; + } + + /** + * Get a exporter driver instance + * + * @param string $type The type to get (yml, xml, etc.) + * @param string $source The directory where the exporter will export to + * @return AbstractExporter $exporter + */ + public function getExporter($type, $dest = null) + { + if ( ! isset(self::$_exporterDrivers[$type])) { + throw ExportException::invalidExporterDriverType($type); + } + + $class = self::$_exporterDrivers[$type]; + + return new $class($dest); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php b/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php new file mode 100644 index 0000000..983d0f6 --- /dev/null +++ b/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php @@ -0,0 +1,205 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * Abstract base class which is to be used for the Exporter drivers + * which can be found in Doctrine\ORM\Tools\Export\Driver + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +abstract class AbstractExporter +{ + protected $_metadata = array(); + protected $_outputDir; + protected $_extension; + + public function __construct($dir = null) + { + $this->_outputDir = $dir; + } + + /** + * Converts a single ClassMetadata instance to the exported format + * and returns it + * + * @param ClassMetadataInfo $metadata + * @return mixed $exported + */ + abstract public function exportClassMetadata(ClassMetadataInfo $metadata); + + /** + * Set the array of ClassMetadataInfo instances to export + * + * @param array $metadata + * @return void + */ + public function setMetadata(array $metadata) + { + $this->_metadata = $metadata; + } + + /** + * Get the extension used to generated the path to a class + * + * @return string $extension + */ + public function getExtension() + { + return $this->_extension; + } + + /** + * Set the directory to output the mapping files to + * + * [php] + * $exporter = new YamlExporter($metadata); + * $exporter->setOutputDir(__DIR__ . '/yaml'); + * $exporter->export(); + * + * @param string $dir + * @return void + */ + public function setOutputDir($dir) + { + $this->_outputDir = $dir; + } + + /** + * Export each ClassMetadata instance to a single Doctrine Mapping file + * named after the entity + * + * @return void + */ + public function export() + { + if ( ! is_dir($this->_outputDir)) { + mkdir($this->_outputDir, 0777, true); + } + + foreach ($this->_metadata as $metadata) { + $output = $this->exportClassMetadata($metadata); + $path = $this->_generateOutputPath($metadata); + $dir = dirname($path); + if ( ! is_dir($dir)) { + mkdir($dir, 0777, true); + } + file_put_contents($path, $output); + } + } + + /** + * Generate the path to write the class for the given ClassMetadataInfo instance + * + * @param ClassMetadataInfo $metadata + * @return string $path + */ + protected function _generateOutputPath(ClassMetadataInfo $metadata) + { + return $this->_outputDir . '/' . str_replace('\\', '.', $metadata->name) . $this->_extension; + } + + /** + * Set the directory to output the mapping files to + * + * [php] + * $exporter = new YamlExporter($metadata, __DIR__ . '/yaml'); + * $exporter->setExtension('.yml'); + * $exporter->export(); + * + * @param string $extension + * @return void + */ + public function setExtension($extension) + { + $this->_extension = $extension; + } + + protected function _getInheritanceTypeString($type) + { + switch ($type) + { + case ClassMetadataInfo::INHERITANCE_TYPE_NONE: + return 'NONE'; + break; + + case ClassMetadataInfo::INHERITANCE_TYPE_JOINED: + return 'JOINED'; + break; + + case ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE: + return 'SINGLE_TABLE'; + break; + + case ClassMetadataInfo::INHERITANCE_TYPE_TABLE_PER_CLASS: + return 'PER_CLASS'; + break; + } + } + + protected function _getChangeTrackingPolicyString($policy) + { + switch ($policy) + { + case ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT: + return 'DEFERRED_IMPLICIT'; + break; + + case ClassMetadataInfo::CHANGETRACKING_DEFERRED_EXPLICIT: + return 'DEFERRED_EXPLICIT'; + break; + + case ClassMetadataInfo::CHANGETRACKING_NOTIFY: + return 'NOTIFY'; + break; + } + } + + protected function _getIdGeneratorTypeString($type) + { + switch ($type) + { + case ClassMetadataInfo::GENERATOR_TYPE_AUTO: + return 'AUTO'; + break; + + case ClassMetadataInfo::GENERATOR_TYPE_SEQUENCE: + return 'SEQUENCE'; + break; + + case ClassMetadataInfo::GENERATOR_TYPE_TABLE: + return 'TABLE'; + break; + + case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY: + return 'IDENTITY'; + break; + } + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/Driver/AnnotationExporter.php b/Doctrine/ORM/Tools/Export/Driver/AnnotationExporter.php new file mode 100644 index 0000000..254905e --- /dev/null +++ b/Doctrine/ORM/Tools/Export/Driver/AnnotationExporter.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo, + Doctrine\ORM\Mapping\AssociationMapping, + Doctrine\ORM\Tools\EntityGenerator; + +/** + * ClassMetadata exporter for PHP classes with annotations + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +class AnnotationExporter extends AbstractExporter +{ + protected $_extension = '.php'; + private $_entityGenerator; + + /** + * Converts a single ClassMetadata instance to the exported format + * and returns it + * + * @param ClassMetadataInfo $metadata + * @return string $exported + */ + public function exportClassMetadata(ClassMetadataInfo $metadata) + { + if ( ! $this->_entityGenerator) { + throw new \RuntimeException('For the AnnotationExporter you must set an EntityGenerator instance with the setEntityGenerator() method.'); + } + $this->_entityGenerator->setGenerateAnnotations(true); + $this->_entityGenerator->setGenerateStubMethods(false); + $this->_entityGenerator->setRegenerateEntityIfExists(false); + $this->_entityGenerator->setUpdateEntityIfExists(false); + + return $this->_entityGenerator->generateEntityClass($metadata); + } + + protected function _generateOutputPath(ClassMetadataInfo $metadata) + { + return $this->_outputDir . '/' . str_replace('\\', '/', $metadata->name) . $this->_extension; + } + + public function setEntityGenerator(EntityGenerator $entityGenerator) + { + $this->_entityGenerator = $entityGenerator; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/Driver/PhpExporter.php b/Doctrine/ORM/Tools/Export/Driver/PhpExporter.php new file mode 100644 index 0000000..005b18a --- /dev/null +++ b/Doctrine/ORM/Tools/Export/Driver/PhpExporter.php @@ -0,0 +1,161 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * ClassMetadata exporter for PHP code + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +class PhpExporter extends AbstractExporter +{ + protected $_extension = '.php'; + + /** + * Converts a single ClassMetadata instance to the exported format + * and returns it + * + * @param ClassMetadataInfo $metadata + * @return mixed $exported + */ + public function exportClassMetadata(ClassMetadataInfo $metadata) + { + $lines = array(); + $lines[] = 'isMappedSuperclass) { + $lines[] = '$metadata->isMappedSuperclass = true;'; + } + + if ($metadata->inheritanceType) { + $lines[] = '$metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_' . $this->_getInheritanceTypeString($metadata->inheritanceType) . ');'; + } + + if ($metadata->customRepositoryClassName) { + $lines[] = "\$metadata->customRepositoryClassName = '" . $metadata->customRepositoryClassName . "';"; + } + + if ($metadata->table) { + $lines[] = '$metadata->setPrimaryTable(' . $this->_varExport($metadata->table) . ');'; + } + + if ($metadata->discriminatorColumn) { + $lines[] = '$metadata->setDiscriminatorColumn(' . $this->_varExport($metadata->discriminatorColumn) . ');'; + } + + if ($metadata->discriminatorMap) { + $lines[] = '$metadata->setDiscriminatorMap(' . $this->_varExport($metadata->discriminatorMap) . ');'; + } + + if ($metadata->changeTrackingPolicy) { + $lines[] = '$metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_' . $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy) . ');'; + } + + if ($metadata->lifecycleCallbacks) { + foreach ($metadata->lifecycleCallbacks as $event => $callbacks) { + foreach ($callbacks as $callback) { + $lines[] = "\$metadata->addLifecycleCallback('$callback', '$event');"; + } + } + } + + foreach ($metadata->fieldMappings as $fieldMapping) { + $lines[] = '$metadata->mapField(' . $this->_varExport($fieldMapping) . ');'; + } + + if ($generatorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { + $lines[] = '$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_' . $generatorType . ');'; + } + + foreach ($metadata->associationMappings as $associationMapping) { + $cascade = array('remove', 'persist', 'refresh', 'merge', 'detach'); + foreach ($cascade as $key => $value) { + if ( ! $associationMapping['isCascade'.ucfirst($value)]) { + unset($cascade[$key]); + } + } + $associationMappingArray = array( + 'fieldName' => $associationMapping['fieldName'], + 'targetEntity' => $associationMapping['targetEntity'], + 'cascade' => $cascade, + ); + + if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { + $method = 'mapOneToOne'; + $oneToOneMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'inversedBy' => $associationMapping['inversedBy'], + 'joinColumns' => $associationMapping['joinColumns'], + 'orphanRemoval' => $associationMapping['orphanRemoval'], + ); + + $associationMappingArray = array_merge($associationMappingArray, $oneToOneMappingArray); + } else if ($associationMapping['type'] == ClassMetadataInfo::ONE_TO_MANY) { + $method = 'mapOneToMany'; + $oneToManyMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'orphanRemoval' => $associationMapping['orphanRemoval'], + 'orderBy' => $associationMapping['orderBy'] + ); + + $associationMappingArray = array_merge($associationMappingArray, $oneToManyMappingArray); + } else if ($associationMapping['type'] == ClassMetadataInfo::MANY_TO_MANY) { + $method = 'mapManyToMany'; + $manyToManyMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'joinTable' => $associationMapping['joinTable'], + 'orderBy' => $associationMapping['orderBy'] + ); + + $associationMappingArray = array_merge($associationMappingArray, $manyToManyMappingArray); + } + + $lines[] = '$metadata->' . $method . '(' . $this->_varExport($associationMappingArray) . ');'; + } + + return implode("\n", $lines); + } + + protected function _varExport($var) + { + $export = var_export($var, true); + $export = str_replace("\n", PHP_EOL . str_repeat(' ', 8), $export); + $export = str_replace(' ', ' ', $export); + $export = str_replace('array (', 'array(', $export); + $export = str_replace('array( ', 'array(', $export); + $export = str_replace(',)', ')', $export); + $export = str_replace(', )', ')', $export); + $export = str_replace(' ', ' ', $export); + + return $export; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php new file mode 100644 index 0000000..fc27ea9 --- /dev/null +++ b/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -0,0 +1,323 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * ClassMetadata exporter for Doctrine XML mapping files + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +class XmlExporter extends AbstractExporter +{ + protected $_extension = '.dcm.xml'; + + /** + * Converts a single ClassMetadata instance to the exported format + * and returns it + * + * @param ClassMetadataInfo $metadata + * @return mixed $exported + */ + public function exportClassMetadata(ClassMetadataInfo $metadata) + { + $xml = new \SimpleXmlElement(""); + + /*$xml->addAttribute('xmlns', 'http://doctrine-project.org/schemas/orm/doctrine-mapping'); + $xml->addAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); + $xml->addAttribute('xsi:schemaLocation', 'http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd');*/ + + if ($metadata->isMappedSuperclass) { + $root = $xml->addChild('mapped-superclass'); + } else { + $root = $xml->addChild('entity'); + } + + if ($metadata->customRepositoryClassName) { + $root->addAttribute('repository-class', $metadata->customRepositoryClassName); + } + + $root->addAttribute('name', $metadata->name); + + if (isset($metadata->table['name'])) { + $root->addAttribute('table', $metadata->table['name']); + } + + if (isset($metadata->table['schema'])) { + $root->addAttribute('schema', $metadata->table['schema']); + } + + if (isset($metadata->table['inheritance-type'])) { + $root->addAttribute('inheritance-type', $metadata->table['inheritance-type']); + } + + if ($metadata->discriminatorColumn) { + $discriminatorColumnXml = $root->addChild('discriminiator-column'); + $discriminatorColumnXml->addAttribute('name', $metadata->discriminatorColumn['name']); + $discriminatorColumnXml->addAttribute('type', $metadata->discriminatorColumn['type']); + $discriminatorColumnXml->addAttribute('length', $metadata->discriminatorColumn['length']); + } + + if ($metadata->discriminatorMap) { + $discriminatorMapXml = $root->addChild('discriminator-map'); + foreach ($metadata->discriminatorMap as $value => $className) { + $discriminatorMappingXml = $discriminatorMapXml->addChild('discriminator-mapping'); + $discriminatorMappingXml->addAttribute('value', $value); + $discriminatorMappingXml->addAttribute('class', $className); + } + } + + $root->addChild('change-tracking-policy', $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy)); + + if (isset($metadata->table['indexes'])) { + $indexesXml = $root->addChild('indexes'); + + foreach ($metadata->table['indexes'] as $name => $index) { + $indexXml = $indexesXml->addChild('index'); + $indexXml->addAttribute('name', $name); + $indexXml->addAttribute('columns', implode(',', $index['columns'])); + } + } + + if (isset($metadata->table['uniqueConstraints'])) { + $uniqueConstraintsXml = $root->addChild('unique-constraints'); + + foreach ($metadata->table['uniqueConstraints'] as $unique) { + $uniqueConstraintXml = $uniqueConstraintsXml->addChild('unique-constraint'); + $uniqueConstraintXml->addAttribute('name', $unique['name']); + $uniqueConstraintXml->addAttribute('columns', implode(',', $unique['columns'])); + } + } + + $fields = $metadata->fieldMappings; + + $id = array(); + foreach ($fields as $name => $field) { + if (isset($field['id']) && $field['id']) { + $id[$name] = $field; + unset($fields[$name]); + } + } + + if ($idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { + $id[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $idGeneratorType; + } + + if ($id) { + foreach ($id as $field) { + $idXml = $root->addChild('id'); + $idXml->addAttribute('name', $field['fieldName']); + $idXml->addAttribute('type', $field['type']); + if (isset($field['columnName'])) { + $idXml->addAttribute('column', $field['columnName']); + } + if ($idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { + $generatorXml = $idXml->addChild('generator'); + $generatorXml->addAttribute('strategy', $idGeneratorType); + } + } + } + + if ($fields) { + foreach ($fields as $field) { + $fieldXml = $root->addChild('field'); + $fieldXml->addAttribute('name', $field['fieldName']); + $fieldXml->addAttribute('type', $field['type']); + if (isset($field['columnName'])) { + $fieldXml->addAttribute('column', $field['columnName']); + } + if (isset($field['length'])) { + $fieldXml->addAttribute('length', $field['length']); + } + if (isset($field['precision'])) { + $fieldXml->addAttribute('precision', $field['precision']); + } + if (isset($field['scale'])) { + $fieldXml->addAttribute('scale', $field['scale']); + } + if (isset($field['unique']) && $field['unique']) { + $fieldXml->addAttribute('unique', $field['unique']); + } + if (isset($field['options'])) { + $optionsXml = $fieldXml->addChild('options'); + foreach ($field['options'] as $key => $value) { + $optionsXml->addAttribute($key, $value); + } + } + if (isset($field['version'])) { + $fieldXml->addAttribute('version', $field['version']); + } + if (isset($field['columnDefinition'])) { + $fieldXml->addAttribute('column-definition', $field['columnDefinition']); + } + } + } + + foreach ($metadata->associationMappings as $name => $associationMapping) { + if ($associationMapping['type'] == ClassMetadataInfo::ONE_TO_ONE) { + $associationMappingXml = $root->addChild('one-to-one'); + } else if ($associationMapping['type'] == ClassMetadataInfo::MANY_TO_ONE) { + $associationMappingXml = $root->addChild('many-to-one'); + } else if ($associationMapping['type'] == ClassMetadataInfo::ONE_TO_MANY) { + $associationMappingXml = $root->addChild('one-to-many'); + } else if ($associationMapping['type'] == ClassMetadataInfo::MANY_TO_MANY) { + $associationMappingXml = $root->addChild('many-to-many'); + } + + $associationMappingXml->addAttribute('field', $associationMapping['fieldName']); + $associationMappingXml->addAttribute('target-entity', $associationMapping['targetEntity']); + + if (isset($associationMapping['mappedBy'])) { + $associationMappingXml->addAttribute('mapped-by', $associationMapping['mappedBy']); + } + if (isset($associationMapping['inversedBy'])) { + $associationMappingXml->addAttribute('inversed-by', $associationMapping['inversedBy']); + } + if (isset($associationMapping['orphanRemoval'])) { + $associationMappingXml->addAttribute('orphan-removal', $associationMapping['orphanRemoval']); + } + if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) { + $joinTableXml = $associationMappingXml->addChild('join-table'); + $joinTableXml->addAttribute('name', $associationMapping['joinTable']['name']); + $joinColumnsXml = $joinTableXml->addChild('join-columns'); + foreach ($associationMapping['joinTable']['joinColumns'] as $joinColumn) { + $joinColumnXml = $joinColumnsXml->addChild('join-column'); + $joinColumnXml->addAttribute('name', $joinColumn['name']); + $joinColumnXml->addAttribute('referenced-column-name', $joinColumn['referencedColumnName']); + if (isset($joinColumn['onDelete'])) { + $joinColumnXml->addAttribute('on-delete', $joinColumn['onDelete']); + } + if (isset($joinColumn['onUpdate'])) { + $joinColumnXml->addAttribute('on-update', $joinColumn['onUpdate']); + } + } + $inverseJoinColumnsXml = $joinTableXml->addChild('inverse-join-columns'); + foreach ($associationMapping['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) { + $inverseJoinColumnXml = $inverseJoinColumnsXml->addChild('join-column'); + $inverseJoinColumnXml->addAttribute('name', $inverseJoinColumn['name']); + $inverseJoinColumnXml->addAttribute('referenced-column-name', $inverseJoinColumn['referencedColumnName']); + if (isset($inverseJoinColumn['onDelete'])) { + $inverseJoinColumnXml->addAttribute('on-delete', $inverseJoinColumn['onDelete']); + } + if (isset($inverseJoinColumn['onUpdate'])) { + $inverseJoinColumnXml->addAttribute('on-update', $inverseJoinColumn['onUpdate']); + } + if (isset($inverseJoinColumn['columnDefinition'])) { + $inverseJoinColumnXml->addAttribute('column-definition', $inverseJoinColumn['columnDefinition']); + } + if (isset($inverseJoinColumn['nullable'])) { + $inverseJoinColumnXml->addAttribute('nullable', $inverseJoinColumn['nullable']); + } + if (isset($inverseJoinColumn['orderBy'])) { + $inverseJoinColumnXml->addAttribute('order-by', $inverseJoinColumn['orderBy']); + } + } + } + if (isset($associationMapping['joinColumns'])) { + $joinColumnsXml = $associationMappingXml->addChild('join-columns'); + foreach ($associationMapping['joinColumns'] as $joinColumn) { + $joinColumnXml = $joinColumnsXml->addChild('join-column'); + $joinColumnXml->addAttribute('name', $joinColumn['name']); + $joinColumnXml->addAttribute('referenced-column-name', $joinColumn['referencedColumnName']); + if (isset($joinColumn['onDelete'])) { + $joinColumnXml->addAttribute('on-delete', $joinColumn['onDelete']); + } + if (isset($joinColumn['onUpdate'])) { + $joinColumnXml->addAttribute('on-update', $joinColumn['onUpdate']); + } + if (isset($joinColumn['columnDefinition'])) { + $joinColumnXml->addAttribute('column-definition', $joinColumn['columnDefinition']); + } + if (isset($joinColumn['nullable'])) { + $joinColumnXml->addAttribute('nullable', $joinColumn['nullable']); + } + } + } + if (isset($associationMapping['orderBy'])) { + $orderByXml = $associationMappingXml->addChild('order-by'); + foreach ($associationMapping['orderBy'] as $name => $direction) { + $orderByFieldXml = $orderByXml->addChild('order-by-field'); + $orderByFieldXml->addAttribute('name', $name); + $orderByFieldXml->addAttribute('direction', $direction); + } + } + $cascade = array(); + if ($associationMapping['isCascadeRemove']) { + $cascade[] = 'cascade-remove'; + } + if ($associationMapping['isCascadePersist']) { + $cascade[] = 'cascade-persist'; + } + if ($associationMapping['isCascadeRefresh']) { + $cascade[] = 'cascade-refresh'; + } + if ($associationMapping['isCascadeMerge']) { + $cascade[] = 'cascade-merge'; + } + if ($associationMapping['isCascadeDetach']) { + $cascade[] = 'cascade-detach'; + } + if ($cascade) { + $cascadeXml = $associationMappingXml->addChild('cascade'); + foreach ($cascade as $type) { + $cascadeXml->addChild($type); + } + } + } + + if (isset($metadata->lifecycleCallbacks)) { + $lifecycleCallbacksXml = $root->addChild('lifecycle-callbacks'); + foreach ($metadata->lifecycleCallbacks as $name => $methods) { + foreach ($methods as $method) { + $lifecycleCallbackXml = $lifecycleCallbacksXml->addChild('lifecycle-callback'); + $lifecycleCallbackXml->addAttribute('type', $name); + $lifecycleCallbackXml->addAttribute('method', $method); + } + } + } + + return $this->_asXml($xml); + } + + /** + * @param \SimpleXMLElement $simpleXml + * @return string $xml + */ + private function _asXml($simpleXml) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->loadXML($simpleXml->asXML()); + $dom->formatOutput = true; + + $result = $dom->saveXML(); + return $result; + } +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php b/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php new file mode 100644 index 0000000..1cc6996 --- /dev/null +++ b/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php @@ -0,0 +1,198 @@ +. + */ + +namespace Doctrine\ORM\Tools\Export\Driver; + +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * ClassMetadata exporter for Doctrine YAML mapping files + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Jonathan Wage + */ +class YamlExporter extends AbstractExporter +{ + protected $_extension = '.dcm.yml'; + + /** + * Converts a single ClassMetadata instance to the exported format + * and returns it + * + * TODO: Should this code be pulled out in to a toArray() method in ClassMetadata + * + * @param ClassMetadataInfo $metadata + * @return mixed $exported + */ + public function exportClassMetadata(ClassMetadataInfo $metadata) + { + $array = array(); + if ($metadata->isMappedSuperclass) { + $array['type'] = 'mappedSuperclass'; + } else { + $array['type'] = 'entity'; + } + $array['table'] = $metadata->table['name']; + + if (isset($metadata->table['schema'])) { + $array['schema'] = $metadata->table['schema']; + } + + $inheritanceType = $metadata->inheritanceType; + if ($inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) { + $array['inheritanceType'] = $this->_getInheritanceTypeString($inheritanceType); + } + + if ($column = $metadata->discriminatorColumn) { + $array['discriminatorColumn'] = $column; + } + + if ($map = $metadata->discriminatorMap) { + $array['discriminatorMap'] = $map; + } + + if ($metadata->changeTrackingPolicy !== ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT) { + $array['changeTrackingPolicy'] = $this->_getChangeTrackingPolicyString($metadata->changeTrackingPolicy); + } + + if (isset($metadata->table['indexes'])) { + $array['indexes'] = $metadata->table['indexes']; + } + + if (isset($metadata->table['uniqueConstraints'])) { + $array['uniqueConstraints'] = $metadata->table['uniqueConstraints']; + } + + $fieldMappings = $metadata->fieldMappings; + + $ids = array(); + foreach ($fieldMappings as $name => $fieldMapping) { + $fieldMapping['column'] = $fieldMapping['columnName']; + unset( + $fieldMapping['columnName'], + $fieldMapping['fieldName'] + ); + + if ($fieldMapping['column'] == $name) { + unset($fieldMapping['column']); + } + + if (isset($fieldMapping['id']) && $fieldMapping['id']) { + $ids[$name] = $fieldMapping; + unset($fieldMappings[$name]); + continue; + } + + $fieldMappings[$name] = $fieldMapping; + } + + if ($idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { + $ids[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $this->_getIdGeneratorTypeString($metadata->generatorType); + } + + if ($ids) { + $array['fields'] = $ids; + } + + if ($fieldMappings) { + if ( ! isset($array['fields'])) { + $array['fields'] = array(); + } + $array['fields'] = array_merge($array['fields'], $fieldMappings); + } + + $associations = array(); + foreach ($metadata->associationMappings as $name => $associationMapping) { + $cascade = array(); + if ($associationMapping['isCascadeRemove']) { + $cascade[] = 'remove'; + } + if ($associationMapping['isCascadePersist']) { + $cascade[] = 'persist'; + } + if ($associationMapping['isCascadeRefresh']) { + $cascade[] = 'refresh'; + } + if ($associationMapping['isCascadeMerge']) { + $cascade[] = 'merge'; + } + if ($associationMapping['isCascadeDetach']) { + $cascade[] = 'detach'; + } + $associationMappingArray = array( + 'targetEntity' => $associationMapping['targetEntity'], + 'cascade' => $cascade, + ); + + if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { + $joinColumns = $associationMapping['joinColumns']; + $newJoinColumns = array(); + foreach ($joinColumns as $joinColumn) { + $newJoinColumns[$joinColumn['name']]['referencedColumnName'] = $joinColumn['referencedColumnName']; + if (isset($joinColumn['onDelete'])) { + $newJoinColumns[$joinColumn['name']]['onDelete'] = $joinColumn['onDelete']; + } + if (isset($joinColumn['onUpdate'])) { + $newJoinColumns[$joinColumn['name']]['onUpdate'] = $joinColumn['onUpdate']; + } + } + $oneToOneMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'inversedBy' => $associationMapping['inversedBy'], + 'joinColumns' => $newJoinColumns, + 'orphanRemoval' => $associationMapping['orphanRemoval'], + ); + + $associationMappingArray = array_merge($associationMappingArray, $oneToOneMappingArray); + $array['oneToOne'][$name] = $associationMappingArray; + } else if ($associationMapping['type'] == ClassMetadataInfo::ONE_TO_MANY) { + $oneToManyMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'inversedBy' => $associationMapping['inversedBy'], + 'orphanRemoval' => $associationMapping['orphanRemoval'], + 'orderBy' => $associationMapping['orderBy'] + ); + + $associationMappingArray = array_merge($associationMappingArray, $oneToManyMappingArray); + $array['oneToMany'][$name] = $associationMappingArray; + } else if ($associationMapping['type'] == ClassMetadataInfo::MANY_TO_MANY) { + $manyToManyMappingArray = array( + 'mappedBy' => $associationMapping['mappedBy'], + 'inversedBy' => $associationMapping['inversedBy'], + 'joinTable' => $associationMapping['joinTable'], + 'orderBy' => isset($associationMapping['orderBy']) ? $associationMapping['orderBy'] : null + ); + + $associationMappingArray = array_merge($associationMappingArray, $manyToManyMappingArray); + $array['manyToMany'][$name] = $associationMappingArray; + } + } + if (isset($metadata->lifecycleCallbacks)) { + $array['lifecycleCallbacks'] = $metadata->lifecycleCallbacks; + } + + return \Symfony\Component\Yaml\Yaml::dump(array($metadata->name => $array), 10); + } +} diff --git a/Doctrine/ORM/Tools/Export/ExportException.php b/Doctrine/ORM/Tools/Export/ExportException.php new file mode 100644 index 0000000..6e7826e --- /dev/null +++ b/Doctrine/ORM/Tools/Export/ExportException.php @@ -0,0 +1,18 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\ORMException, + Doctrine\DBAL\Types\Type, + Doctrine\ORM\EntityManager, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\Internal\CommitOrderCalculator, + Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs, + Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; + +/** + * The SchemaTool is a tool to create/drop/update database schemas based on + * ClassMetadata class descriptors. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Benjamin Eberlei + */ +class SchemaTool +{ + /** + * @var \Doctrine\ORM\EntityManager + */ + private $_em; + + /** + * @var \Doctrine\DBAL\Platforms\AbstractPlatform + */ + private $_platform; + + /** + * Initializes a new SchemaTool instance that uses the connection of the + * provided EntityManager. + * + * @param Doctrine\ORM\EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->_em = $em; + $this->_platform = $em->getConnection()->getDatabasePlatform(); + } + + /** + * Creates the database schema for the given array of ClassMetadata instances. + * + * @param array $classes + */ + public function createSchema(array $classes) + { + $createSchemaSql = $this->getCreateSchemaSql($classes); + $conn = $this->_em->getConnection(); + + foreach ($createSchemaSql as $sql) { + $conn->executeQuery($sql); + } + } + + /** + * Gets the list of DDL statements that are required to create the database schema for + * the given list of ClassMetadata instances. + * + * @param array $classes + * @return array $sql The SQL statements needed to create the schema for the classes. + */ + public function getCreateSchemaSql(array $classes) + { + $schema = $this->getSchemaFromMetadata($classes); + return $schema->toSql($this->_platform); + } + + /** + * Some instances of ClassMetadata don't need to be processed in the SchemaTool context. This method detects them. + * + * @param ClassMetadata $class + * @param array $processedClasses + * @return bool + */ + private function processingNotRequired($class, array $processedClasses) + { + return ( + isset($processedClasses[$class->name]) || + $class->isMappedSuperclass || + ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) + ); + } + + /** + * From a given set of metadata classes this method creates a Schema instance. + * + * @param array $classes + * @return Schema + */ + public function getSchemaFromMetadata(array $classes) + { + $processedClasses = array(); // Reminder for processed classes, used for hierarchies + + $sm = $this->_em->getConnection()->getSchemaManager(); + $metadataSchemaConfig = $sm->createSchemaConfig(); + $metadataSchemaConfig->setExplicitForeignKeyIndexes(false); + $schema = new \Doctrine\DBAL\Schema\Schema(array(), array(), $metadataSchemaConfig); + + $evm = $this->_em->getEventManager(); + + foreach ($classes as $class) { + if ($this->processingNotRequired($class, $processedClasses)) { + continue; + } + + $table = $schema->createTable($class->getQuotedTableName($this->_platform)); + + // TODO: Remove + /**if ($class->isIdGeneratorIdentity()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_IDENTITY); + } else if ($class->isIdGeneratorSequence()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_SEQUENCE); + }*/ + + $columns = array(); // table columns + + if ($class->isInheritanceTypeSingleTable()) { + $columns = $this->_gatherColumns($class, $table); + $this->_gatherRelationsSql($class, $table, $schema); + + // Add the discriminator column + $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + + // Aggregate all the information from all classes in the hierarchy + foreach ($class->parentClasses as $parentClassName) { + // Parent class information is already contained in this class + $processedClasses[$parentClassName] = true; + } + + foreach ($class->subClasses as $subClassName) { + $subClass = $this->_em->getClassMetadata($subClassName); + $this->_gatherColumns($subClass, $table); + $this->_gatherRelationsSql($subClass, $table, $schema); + $processedClasses[$subClassName] = true; + } + } else if ($class->isInheritanceTypeJoined()) { + // Add all non-inherited fields as columns + $pkColumns = array(); + foreach ($class->fieldMappings as $fieldName => $mapping) { + if ( ! isset($mapping['inherited'])) { + $columnName = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + $this->_gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($fieldName)) { + $pkColumns[] = $columnName; + } + } + } + + $this->_gatherRelationsSql($class, $table, $schema); + + // Add the discriminator column only to the root table + if ($class->name == $class->rootEntityName) { + $discrColumnDef = $this->_getDiscriminatorColumnDefinition($class, $table); + } else { + // Add an ID FK column to child tables + /* @var Doctrine\ORM\Mapping\ClassMetadata $class */ + $idMapping = $class->fieldMappings[$class->identifier[0]]; + $this->_gatherColumn($class, $idMapping, $table); + $columnName = $class->getQuotedColumnName($class->identifier[0], $this->_platform); + // TODO: This seems rather hackish, can we optimize it? + $table->getColumn($class->identifier[0])->setAutoincrement(false); + + $pkColumns[] = $columnName; + // TODO: REMOVE + /*if ($table->isIdGeneratorIdentity()) { + $table->setIdGeneratorType(\Doctrine\DBAL\Schema\Table::ID_NONE); + }*/ + + // Add a FK constraint on the ID column + $table->addUnnamedForeignKeyConstraint( + $this->_em->getClassMetadata($class->rootEntityName)->getTableName(), + array($columnName), array($columnName), array('onDelete' => 'CASCADE') + ); + } + + $table->setPrimaryKey($pkColumns); + + } else if ($class->isInheritanceTypeTablePerClass()) { + throw ORMException::notSupported(); + } else { + $this->_gatherColumns($class, $table); + $this->_gatherRelationsSql($class, $table, $schema); + } + + if (isset($class->table['indexes'])) { + foreach ($class->table['indexes'] AS $indexName => $indexData) { + $table->addIndex($indexData['columns'], $indexName); + } + } + + if (isset($class->table['uniqueConstraints'])) { + foreach ($class->table['uniqueConstraints'] AS $indexName => $indexData) { + $table->addUniqueIndex($indexData['columns'], $indexName); + } + } + + $processedClasses[$class->name] = true; + + if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) { + $seqDef = $class->sequenceGeneratorDefinition; + + if (!$schema->hasSequence($seqDef['sequenceName'])) { + $schema->createSequence( + $seqDef['sequenceName'], + $seqDef['allocationSize'], + $seqDef['initialValue'] + ); + } + } + + if ($evm->hasListeners(ToolEvents::postGenerateSchemaTable)) { + $evm->dispatchEvent(ToolEvents::postGenerateSchemaTable, new GenerateSchemaTableEventArgs($class, $schema, $table)); + } + } + + if ($evm->hasListeners(ToolEvents::postGenerateSchema)) { + $evm->dispatchEvent(ToolEvents::postGenerateSchema, new GenerateSchemaEventArgs($this->_em, $schema)); + } + + return $schema; + } + + /** + * Gets a portable column definition as required by the DBAL for the discriminator + * column of a class. + * + * @param ClassMetadata $class + * @return array The portable column definition of the discriminator column as required by + * the DBAL. + */ + private function _getDiscriminatorColumnDefinition($class, $table) + { + $discrColumn = $class->discriminatorColumn; + + if (!isset($discrColumn['type']) || (strtolower($discrColumn['type']) == 'string' && $discrColumn['length'] === null)) { + $discrColumn['type'] = 'string'; + $discrColumn['length'] = 255; + } + + $table->addColumn( + $discrColumn['name'], + $discrColumn['type'], + array('length' => $discrColumn['length'], 'notnull' => true) + ); + } + + /** + * Gathers the column definitions as required by the DBAL of all field mappings + * found in the given class. + * + * @param ClassMetadata $class + * @param Table $table + * @return array The list of portable column definitions as required by the DBAL. + */ + private function _gatherColumns($class, $table) + { + $columns = array(); + $pkColumns = array(); + + foreach ($class->fieldMappings as $fieldName => $mapping) { + $column = $this->_gatherColumn($class, $mapping, $table); + + if ($class->isIdentifier($mapping['fieldName'])) { + $pkColumns[] = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + } + } + // For now, this is a hack required for single table inheritence, since this method is called + // twice by single table inheritence relations + if(!$table->hasIndex('primary')) { + $table->setPrimaryKey($pkColumns); + } + + return $columns; + } + + /** + * Creates a column definition as required by the DBAL from an ORM field mapping definition. + * + * @param ClassMetadata $class The class that owns the field mapping. + * @param array $mapping The field mapping. + * @param Table $table + * @return array The portable column definition as required by the DBAL. + */ + private function _gatherColumn($class, array $mapping, $table) + { + $columnName = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform); + $columnType = $mapping['type']; + + $options = array(); + $options['length'] = isset($mapping['length']) ? $mapping['length'] : null; + $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true; + if ($class->isInheritanceTypeSingleTable() && count($class->parentClasses) > 0) { + $options['notnull'] = false; + } + + $options['platformOptions'] = array(); + $options['platformOptions']['version'] = $class->isVersioned && $class->versionField == $mapping['fieldName'] ? true : false; + + if(strtolower($columnType) == 'string' && $options['length'] === null) { + $options['length'] = 255; + } + + if (isset($mapping['precision'])) { + $options['precision'] = $mapping['precision']; + } + + if (isset($mapping['scale'])) { + $options['scale'] = $mapping['scale']; + } + + if (isset($mapping['default'])) { + $options['default'] = $mapping['default']; + } + + if (isset($mapping['columnDefinition'])) { + $options['columnDefinition'] = $mapping['columnDefinition']; + } + + if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == array($mapping['fieldName'])) { + $options['autoincrement'] = true; + } + + if ($table->hasColumn($columnName)) { + // required in some inheritance scenarios + $table->changeColumn($columnName, $options); + } else { + $table->addColumn($columnName, $columnType, $options); + } + + $isUnique = isset($mapping['unique']) ? $mapping['unique'] : false; + if ($isUnique) { + $table->addUniqueIndex(array($columnName)); + } + } + + /** + * Gathers the SQL for properly setting up the relations of the given class. + * This includes the SQL for foreign key constraints and join tables. + * + * @param ClassMetadata $class + * @param \Doctrine\DBAL\Schema\Table $table + * @param \Doctrine\DBAL\Schema\Schema $schema + * @return void + */ + private function _gatherRelationsSql($class, $table, $schema) + { + foreach ($class->associationMappings as $fieldName => $mapping) { + if (isset($mapping['inherited'])) { + continue; + } + + $foreignClass = $this->_em->getClassMetadata($mapping['targetEntity']); + + if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) { + $primaryKeyColumns = $uniqueConstraints = array(); // PK is unnecessary for this relation-type + + $this->_gatherRelationJoinColumns($mapping['joinColumns'], $table, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints); + + foreach($uniqueConstraints AS $indexName => $unique) { + $table->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); + } + } else if ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) { + //... create join table, one-many through join table supported later + throw ORMException::notSupported(); + } else if ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) { + // create join table + $joinTable = $mapping['joinTable']; + + $theJoinTable = $schema->createTable($foreignClass->getQuotedJoinTableName($mapping, $this->_platform)); + + $primaryKeyColumns = $uniqueConstraints = array(); + + // Build first FK constraint (relation table => source table) + $this->_gatherRelationJoinColumns($joinTable['joinColumns'], $theJoinTable, $class, $mapping, $primaryKeyColumns, $uniqueConstraints); + + // Build second FK constraint (relation table => target table) + $this->_gatherRelationJoinColumns($joinTable['inverseJoinColumns'], $theJoinTable, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints); + + $theJoinTable->setPrimaryKey($primaryKeyColumns); + + foreach($uniqueConstraints AS $indexName => $unique) { + $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); + } + } + } + } + + /** + * Gather columns and fk constraints that are required for one part of relationship. + * + * @param array $joinColumns + * @param \Doctrine\DBAL\Schema\Table $theJoinTable + * @param ClassMetadata $class + * @param \Doctrine\ORM\Mapping\AssociationMapping $mapping + * @param array $primaryKeyColumns + * @param array $uniqueConstraints + */ + private function _gatherRelationJoinColumns($joinColumns, $theJoinTable, $class, $mapping, &$primaryKeyColumns, &$uniqueConstraints) + { + $localColumns = array(); + $foreignColumns = array(); + $fkOptions = array(); + + foreach ($joinColumns as $joinColumn) { + $columnName = $joinColumn['name']; + $referencedFieldName = $class->getFieldName($joinColumn['referencedColumnName']); + + if ( ! $class->hasField($referencedFieldName)) { + throw new \Doctrine\ORM\ORMException( + "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ". + $mapping['sourceEntity'] . " towards ". $mapping['targetEntity'] . " does not exist." + ); + } + + $primaryKeyColumns[] = $columnName; + $localColumns[] = $columnName; + $foreignColumns[] = $joinColumn['referencedColumnName']; + + if ( ! $theJoinTable->hasColumn($joinColumn['name'])) { + // Only add the column to the table if it does not exist already. + // It might exist already if the foreign key is mapped into a regular + // property as well. + + $fieldMapping = $class->getFieldMapping($referencedFieldName); + + $columnDef = null; + if (isset($joinColumn['columnDefinition'])) { + $columnDef = $joinColumn['columnDefinition']; + } else if (isset($fieldMapping['columnDefinition'])) { + $columnDef = $fieldMapping['columnDefinition']; + } + $columnOptions = array('notnull' => false, 'columnDefinition' => $columnDef); + if (isset($joinColumn['nullable'])) { + $columnOptions['notnull'] = !$joinColumn['nullable']; + } + if ($fieldMapping['type'] == "string") { + $columnOptions['length'] = $fieldMapping['length']; + } else if ($fieldMapping['type'] == "decimal") { + $columnOptions['scale'] = $fieldMapping['scale']; + $columnOptions['precision'] = $fieldMapping['precision']; + } + + $theJoinTable->addColumn( + $columnName, $class->getTypeOfColumn($joinColumn['referencedColumnName']), $columnOptions + ); + } + + if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) { + $uniqueConstraints[] = array('columns' => array($columnName)); + } + + if (isset($joinColumn['onUpdate'])) { + $fkOptions['onUpdate'] = $joinColumn['onUpdate']; + } + + if (isset($joinColumn['onDelete'])) { + $fkOptions['onDelete'] = $joinColumn['onDelete']; + } + } + + $theJoinTable->addUnnamedForeignKeyConstraint( + $class->getTableName(), $localColumns, $foreignColumns, $fkOptions + ); + } + + /** + * Drops the database schema for the given classes. + * + * In any way when an exception is thrown it is supressed since drop was + * issued for all classes of the schema and some probably just don't exist. + * + * @param array $classes + * @return void + */ + public function dropSchema(array $classes) + { + $dropSchemaSql = $this->getDropSchemaSql($classes); + $conn = $this->_em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + $conn->executeQuery($sql); + } + } + + /** + * Drops all elements in the database of the current connection. + * + * @return void + */ + public function dropDatabase() + { + $dropSchemaSql = $this->getDropDatabaseSQL(); + $conn = $this->_em->getConnection(); + + foreach ($dropSchemaSql as $sql) { + $conn->executeQuery($sql); + } + } + + /** + * Gets the SQL needed to drop the database schema for the connections database. + * + * @return array + */ + public function getDropDatabaseSQL() + { + $sm = $this->_em->getConnection()->getSchemaManager(); + $schema = $sm->createSchema(); + + $visitor = new \Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector($this->_platform); + /* @var $schema \Doctrine\DBAL\Schema\Schema */ + $schema->visit($visitor); + return $visitor->getQueries(); + } + + /** + * + * @param array $classes + * @return array + */ + public function getDropSchemaSQL(array $classes) + { + $sm = $this->_em->getConnection()->getSchemaManager(); + + $sql = array(); + $orderedTables = array(); + + foreach ($classes AS $class) { + if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName && $this->_platform->supportsSequences()) { + $sql[] = $this->_platform->getDropSequenceSQL($class->sequenceGeneratorDefinition['sequenceName']); + } + } + + $commitOrder = $this->_getCommitOrder($classes); + $associationTables = $this->_getAssociationTables($commitOrder); + + // Drop association tables first + foreach ($associationTables as $associationTable) { + if (!in_array($associationTable, $orderedTables)) { + $orderedTables[] = $associationTable; + } + } + + // Drop tables in reverse commit order + for ($i = count($commitOrder) - 1; $i >= 0; --$i) { + $class = $commitOrder[$i]; + + if (($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) + || $class->isMappedSuperclass) { + continue; + } + + if (!in_array($class->getTableName(), $orderedTables)) { + $orderedTables[] = $class->getTableName(); + } + } + + $dropTablesSql = array(); + foreach ($orderedTables AS $tableName) { + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $foreignKeys = $sm->listTableForeignKeys($tableName); + foreach ($foreignKeys AS $foreignKey) { + $sql[] = $this->_platform->getDropForeignKeySQL($foreignKey, $tableName); + } + $dropTablesSql[] = $this->_platform->getDropTableSQL($tableName); + } + + return array_merge($sql, $dropTablesSql); + } + + /** + * Updates the database schema of the given classes by comparing the ClassMetadata + * ins$tableNametances to the current database schema that is inspected. + * + * @param array $classes + * @return void + */ + public function updateSchema(array $classes, $saveMode=false) + { + $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode); + $conn = $this->_em->getConnection(); + + foreach ($updateSchemaSql as $sql) { + $conn->executeQuery($sql); + } + } + + /** + * Gets the sequence of SQL statements that need to be performed in order + * to bring the given class mappings in-synch with the relational schema. + * + * @param array $classes The classes to consider. + * @return array The sequence of SQL statements. + */ + public function getUpdateSchemaSql(array $classes, $saveMode=false) + { + $sm = $this->_em->getConnection()->getSchemaManager(); + + $fromSchema = $sm->createSchema(); + $toSchema = $this->getSchemaFromMetadata($classes); + + $comparator = new \Doctrine\DBAL\Schema\Comparator(); + $schemaDiff = $comparator->compare($fromSchema, $toSchema); + + if ($saveMode) { + return $schemaDiff->toSaveSql($this->_platform); + } else { + return $schemaDiff->toSql($this->_platform); + } + } + + private function _getCommitOrder(array $classes) + { + $calc = new CommitOrderCalculator; + + // Calculate dependencies + foreach ($classes as $class) { + $calc->addClass($class); + + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide']) { + $targetClass = $this->_em->getClassMetadata($assoc['targetEntity']); + + if ( ! $calc->hasClass($targetClass->name)) { + $calc->addClass($targetClass); + } + + // add dependency ($targetClass before $class) + $calc->addDependency($targetClass, $class); + } + } + } + + return $calc->getCommitOrder(); + } + + private function _getAssociationTables(array $classes) + { + $associationTables = array(); + + foreach ($classes as $class) { + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { + $associationTables[] = $assoc['joinTable']['name']; + } + } + } + + return $associationTables; + } +} diff --git a/Doctrine/ORM/Tools/SchemaValidator.php b/Doctrine/ORM/Tools/SchemaValidator.php new file mode 100644 index 0000000..8acaa0b --- /dev/null +++ b/Doctrine/ORM/Tools/SchemaValidator.php @@ -0,0 +1,219 @@ +. +*/ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\ClassMetadataInfo; + +/** + * Performs strict validation of the mapping schema + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class SchemaValidator +{ + /** + * @var EntityManager + */ + private $em; + + /** + * @param EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Checks the internal consistency of mapping files. + * + * There are several checks that can't be done at runtime or are too expensive, which can be verified + * with this command. For example: + * + * 1. Check if a relation with "mappedBy" is actually connected to that specified field. + * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. + * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. + * 4. Check if there are public properties that might cause problems with lazy loading. + * + * @return array + */ + public function validateMapping() + { + $errors = array(); + $cmf = $this->em->getMetadataFactory(); + $classes = $cmf->getAllMetadata(); + + foreach ($classes AS $class) { + $ce = array(); + /* @var $class ClassMetadata */ + foreach ($class->associationMappings AS $fieldName => $assoc) { + if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { + $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; + } + + if ($assoc['mappedBy'] && $assoc['inversedBy']) { + $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; + } + + $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); + + /* @var $assoc AssociationMapping */ + if ($assoc['mappedBy']) { + if ($targetMetadata->hasField($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['inversedBy']) { + if ($targetMetadata->hasField($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['isOwningSide']) { + if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { + foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { + if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { + $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); + if (!isset($targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']])) { + $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; + break; + } + + $fieldName = $targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetClass->identifier)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { + foreach ($assoc['joinColumns'] AS $joinColumn) { + $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); + if (!isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; + break; + } + + $fieldName = $targetClass->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetClass->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + } + } + + if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { + $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); + foreach ($assoc['orderBy'] AS $orderField => $orientation) { + if (!$targetClass->hasField($orderField)) { + $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . + $orderField . " that is not a field on the target entity " . $targetClass->name; + } + } + } + } + + foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { + if ($publicAttr->isStatic()) { + continue; + } + $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". + "or protected. Public fields may break lazy-loading."; + } + + foreach ($class->subClasses AS $subClass) { + if (!in_array($class->name, class_parents($subClass))) { + $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". + "of '" . $class->name . "' but these entities are not related through inheritance."; + } + } + + if ($ce) { + $errors[$class->name] = $ce; + } + } + + return $errors; + } + + /** + * Check if the Database Schema is in sync with the current metadata state. + * + * @return bool + */ + public function schemaInSyncWithMetadata() + { + $schemaTool = new SchemaTool($this->em); + + $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); + return (count($schemaTool->getUpdateSchemaSql($allMetadata, false)) == 0); + } +} diff --git a/Doctrine/ORM/Tools/ToolEvents.php b/Doctrine/ORM/Tools/ToolEvents.php new file mode 100644 index 0000000..7ea1f57 --- /dev/null +++ b/Doctrine/ORM/Tools/ToolEvents.php @@ -0,0 +1,44 @@ +. +*/ + +namespace Doctrine\ORM\Tools; + +class ToolEvents +{ + /** + * The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata() + * whenever an entity class is transformed into its table representation. It recieves + * the current non-complete Schema instance, the Entity Metadata Class instance and + * the Schema Table instance of this entity. + * + * @var string + */ + const postGenerateSchemaTable = 'postGenerateSchemaTable'; + + /** + * The postGenerateSchema event is triggered in SchemaTool#getSchemaFromMetadata() + * after all entity classes have been transformed into the related Schema structure. + * The EventArgs contain the EntityManager and the created Schema instance. + * + * @var string + */ + const postGenerateSchema = 'postGenerateSchema'; +} \ No newline at end of file diff --git a/Doctrine/ORM/Tools/ToolsException.php b/Doctrine/ORM/Tools/ToolsException.php new file mode 100644 index 0000000..f7ed871 --- /dev/null +++ b/Doctrine/ORM/Tools/ToolsException.php @@ -0,0 +1,13 @@ +. +*/ + +namespace Doctrine\ORM; + +/** + * Is thrown when a transaction is required for the current operation, but there is none open. + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.com + * @since 1.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Roman Borschel + */ +class TransactionRequiredException extends ORMException +{ + static public function transactionRequired() + { + return new self('An open transaction is required for this operation.'); + } +} \ No newline at end of file diff --git a/Doctrine/ORM/UnitOfWork.php b/Doctrine/ORM/UnitOfWork.php new file mode 100644 index 0000000..5088bb2 --- /dev/null +++ b/Doctrine/ORM/UnitOfWork.php @@ -0,0 +1,2262 @@ +. + */ + +namespace Doctrine\ORM; + +use Exception, InvalidArgumentException, UnexpectedValueException, + Doctrine\Common\Collections\ArrayCollection, + Doctrine\Common\Collections\Collection, + Doctrine\Common\NotifyPropertyChanged, + Doctrine\Common\PropertyChangedListener, + Doctrine\ORM\Event\LifecycleEventArgs, + Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\Proxy\Proxy; + +/** + * The UnitOfWork is responsible for tracking changes to objects during an + * "object-level" transaction and for writing out changes to the database + * in the correct order. + * + * @since 2.0 + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @internal This class contains highly performance-sensitive code. + */ +class UnitOfWork implements PropertyChangedListener +{ + /** + * An entity is in MANAGED state when its persistence is managed by an EntityManager. + */ + const STATE_MANAGED = 1; + + /** + * An entity is new if it has just been instantiated (i.e. using the "new" operator) + * and is not (yet) managed by an EntityManager. + */ + const STATE_NEW = 2; + + /** + * A detached entity is an instance with persistent state and identity that is not + * (or no longer) associated with an EntityManager (and a UnitOfWork). + */ + const STATE_DETACHED = 3; + + /** + * A removed entity instance is an instance with a persistent identity, + * associated with an EntityManager, whose persistent state will be deleted + * on commit. + */ + const STATE_REMOVED = 4; + + /** + * The identity map that holds references to all managed entities that have + * an identity. The entities are grouped by their class name. + * Since all classes in a hierarchy must share the same identifier set, + * we always take the root class name of the hierarchy. + * + * @var array + */ + private $identityMap = array(); + + /** + * Map of all identifiers of managed entities. + * Keys are object ids (spl_object_hash). + * + * @var array + */ + private $entityIdentifiers = array(); + + /** + * Map of the original entity data of managed entities. + * Keys are object ids (spl_object_hash). This is used for calculating changesets + * at commit time. + * + * @var array + * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage. + * A value will only really be copied if the value in the entity is modified + * by the user. + */ + private $originalEntityData = array(); + + /** + * Map of entity changes. Keys are object ids (spl_object_hash). + * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. + * + * @var array + */ + private $entityChangeSets = array(); + + /** + * The (cached) states of any known entities. + * Keys are object ids (spl_object_hash). + * + * @var array + */ + private $entityStates = array(); + + /** + * Map of entities that are scheduled for dirty checking at commit time. + * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. + * Keys are object ids (spl_object_hash). + * + * @var array + * @todo rename: scheduledForSynchronization + */ + private $scheduledForDirtyCheck = array(); + + /** + * A list of all pending entity insertions. + * + * @var array + */ + private $entityInsertions = array(); + + /** + * A list of all pending entity updates. + * + * @var array + */ + private $entityUpdates = array(); + + /** + * Any pending extra updates that have been scheduled by persisters. + * + * @var array + */ + private $extraUpdates = array(); + + /** + * A list of all pending entity deletions. + * + * @var array + */ + private $entityDeletions = array(); + + /** + * All pending collection deletions. + * + * @var array + */ + private $collectionDeletions = array(); + + /** + * All pending collection updates. + * + * @var array + */ + private $collectionUpdates = array(); + + /** + * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. + * At the end of the UnitOfWork all these collections will make new snapshots + * of their data. + * + * @var array + */ + private $visitedCollections = array(); + + /** + * The EntityManager that "owns" this UnitOfWork instance. + * + * @var Doctrine\ORM\EntityManager + */ + private $em; + + /** + * The calculator used to calculate the order in which changes to + * entities need to be written to the database. + * + * @var Doctrine\ORM\Internal\CommitOrderCalculator + */ + private $commitOrderCalculator; + + /** + * The entity persister instances used to persist entity instances. + * + * @var array + */ + private $persisters = array(); + + /** + * The collection persister instances used to persist collections. + * + * @var array + */ + private $collectionPersisters = array(); + + /** + * The EventManager used for dispatching events. + * + * @var EventManager + */ + private $evm; + + /** + * Orphaned entities that are scheduled for removal. + * + * @var array + */ + private $orphanRemovals = array(); + + //private $_readOnlyObjects = array(); + + /** + * Initializes a new UnitOfWork instance, bound to the given EntityManager. + * + * @param Doctrine\ORM\EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + $this->evm = $em->getEventManager(); + } + + /** + * Commits the UnitOfWork, executing all operations that have been postponed + * up to this point. The state of all managed entities will be synchronized with + * the database. + * + * The operations are executed in the following order: + * + * 1) All entity insertions + * 2) All entity updates + * 3) All collection deletions + * 4) All collection updates + * 5) All entity deletions + * + */ + public function commit() + { + // Compute changes done since last commit. + $this->computeChangeSets(); + + if ( ! ($this->entityInsertions || + $this->entityDeletions || + $this->entityUpdates || + $this->collectionUpdates || + $this->collectionDeletions || + $this->orphanRemovals)) { + return; // Nothing to do. + } + + if ($this->orphanRemovals) { + foreach ($this->orphanRemovals as $orphan) { + $this->remove($orphan); + } + } + + // Raise onFlush + if ($this->evm->hasListeners(Events::onFlush)) { + $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->em)); + } + + // Now we need a commit order to maintain referential integrity + $commitOrder = $this->getCommitOrder(); + + $conn = $this->em->getConnection(); + + $conn->beginTransaction(); + try { + if ($this->entityInsertions) { + foreach ($commitOrder as $class) { + $this->executeInserts($class); + } + } + + if ($this->entityUpdates) { + foreach ($commitOrder as $class) { + $this->executeUpdates($class); + } + } + + // Extra updates that were requested by persisters. + if ($this->extraUpdates) { + $this->executeExtraUpdates(); + } + + // Collection deletions (deletions of complete collections) + foreach ($this->collectionDeletions as $collectionToDelete) { + $this->getCollectionPersister($collectionToDelete->getMapping()) + ->delete($collectionToDelete); + } + // Collection updates (deleteRows, updateRows, insertRows) + foreach ($this->collectionUpdates as $collectionToUpdate) { + $this->getCollectionPersister($collectionToUpdate->getMapping()) + ->update($collectionToUpdate); + } + + // Entity deletions come last and need to be in reverse commit order + if ($this->entityDeletions) { + for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) { + $this->executeDeletions($commitOrder[$i]); + } + } + + $conn->commit(); + } catch (Exception $e) { + $this->em->close(); + $conn->rollback(); + throw $e; + } + + // Take new snapshots from visited collections + foreach ($this->visitedCollections as $coll) { + $coll->takeSnapshot(); + } + + // Clear up + $this->entityInsertions = + $this->entityUpdates = + $this->entityDeletions = + $this->extraUpdates = + $this->entityChangeSets = + $this->collectionUpdates = + $this->collectionDeletions = + $this->visitedCollections = + $this->scheduledForDirtyCheck = + $this->orphanRemovals = array(); + } + + /** + * Executes any extra updates that have been scheduled. + */ + private function executeExtraUpdates() + { + foreach ($this->extraUpdates as $oid => $update) { + list ($entity, $changeset) = $update; + $this->entityChangeSets[$oid] = $changeset; + $this->getEntityPersister(get_class($entity))->update($entity); + } + } + + /** + * Gets the changeset for an entity. + * + * @return array + */ + public function getEntityChangeSet($entity) + { + $oid = spl_object_hash($entity); + if (isset($this->entityChangeSets[$oid])) { + return $this->entityChangeSets[$oid]; + } + return array(); + } + + /** + * Computes the changes that happened to a single entity. + * + * Modifies/populates the following properties: + * + * {@link _originalEntityData} + * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) + * then it was not fetched from the database and therefore we have no original + * entity data yet. All of the current entity data is stored as the original entity data. + * + * {@link _entityChangeSets} + * The changes detected on all properties of the entity are stored there. + * A change is a tuple array where the first entry is the old value and the second + * entry is the new value of the property. Changesets are used by persisters + * to INSERT/UPDATE the persistent entity state. + * + * {@link _entityUpdates} + * If the entity is already fully MANAGED (has been fetched from the database before) + * and any changes to its properties are detected, then a reference to the entity is stored + * there to mark it for an update. + * + * {@link _collectionDeletions} + * If a PersistentCollection has been de-referenced in a fully MANAGED entity, + * then this collection is marked for deletion. + * + * @param ClassMetadata $class The class descriptor of the entity. + * @param object $entity The entity for which to compute the changes. + */ + public function computeChangeSet(ClassMetadata $class, $entity) + { + if ( ! $class->isInheritanceTypeNone()) { + $class = $this->em->getClassMetadata(get_class($entity)); + } + + $oid = spl_object_hash($entity); + $actualData = array(); + foreach ($class->reflFields as $name => $refProp) { + $value = $refProp->getValue($entity); + if ($class->isCollectionValuedAssociation($name) && $value !== null + && ! ($value instanceof PersistentCollection)) { + // If $value is not a Collection then use an ArrayCollection. + if ( ! $value instanceof Collection) { + $value = new ArrayCollection($value); + } + + $assoc = $class->associationMappings[$name]; + + // Inject PersistentCollection + $coll = new PersistentCollection( + $this->em, + $this->em->getClassMetadata($assoc['targetEntity']), + $value + ); + + $coll->setOwner($entity, $assoc); + $coll->setDirty( ! $coll->isEmpty()); + $class->reflFields[$name]->setValue($entity, $coll); + $actualData[$name] = $coll; + } else if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { + $actualData[$name] = $value; + } + } + + if ( ! isset($this->originalEntityData[$oid])) { + // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). + // These result in an INSERT. + $this->originalEntityData[$oid] = $actualData; + $changeSet = array(); + foreach ($actualData as $propName => $actualValue) { + if (isset($class->associationMappings[$propName])) { + $assoc = $class->associationMappings[$propName]; + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + $changeSet[$propName] = array(null, $actualValue); + } + } else { + $changeSet[$propName] = array(null, $actualValue); + } + } + $this->entityChangeSets[$oid] = $changeSet; + } else { + // Entity is "fully" MANAGED: it was already fully persisted before + // and we have a copy of the original data + $originalData = $this->originalEntityData[$oid]; + $isChangeTrackingNotify = $class->isChangeTrackingNotify(); + $changeSet = $isChangeTrackingNotify ? $this->entityChangeSets[$oid] : array(); + + foreach ($actualData as $propName => $actualValue) { + $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; + if (isset($class->associationMappings[$propName])) { + $assoc = $class->associationMappings[$propName]; + if ($assoc['type'] & ClassMetadata::TO_ONE && $orgValue !== $actualValue) { + if ($assoc['isOwningSide']) { + $changeSet[$propName] = array($orgValue, $actualValue); + } + if ($orgValue !== null && $assoc['orphanRemoval']) { + $this->scheduleOrphanRemoval($orgValue); + } + } else if ($orgValue instanceof PersistentCollection && $orgValue !== $actualValue) { + // A PersistentCollection was de-referenced, so delete it. + if ( ! in_array($orgValue, $this->collectionDeletions, true)) { + $this->collectionDeletions[] = $orgValue; + } + } + } else if ($isChangeTrackingNotify) { + continue; + } else if (is_object($orgValue) && $orgValue !== $actualValue) { + $changeSet[$propName] = array($orgValue, $actualValue); + } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { + $changeSet[$propName] = array($orgValue, $actualValue); + } + } + if ($changeSet) { + $this->entityChangeSets[$oid] = $changeSet; + $this->originalEntityData[$oid] = $actualData; + $this->entityUpdates[$oid] = $entity; + } + } + + // Look for changes in associations of the entity + foreach ($class->associationMappings as $field => $assoc) { + $val = $class->reflFields[$field]->getValue($entity); + if ($val !== null) { + $this->computeAssociationChanges($assoc, $val); + } + } + } + + /** + * Computes all the changes that have been done to entities and collections + * since the last commit and stores these changes in the _entityChangeSet map + * temporarily for access by the persisters, until the UoW commit is finished. + */ + public function computeChangeSets() + { + // Compute changes for INSERTed entities first. This must always happen. + foreach ($this->entityInsertions as $entity) { + $class = $this->em->getClassMetadata(get_class($entity)); + $this->computeChangeSet($class, $entity); + } + + // Compute changes for other MANAGED entities. Change tracking policies take effect here. + foreach ($this->identityMap as $className => $entities) { + $class = $this->em->getClassMetadata($className); + + // Skip class if instances are read-only + //if ($class->isReadOnly) { + // continue; + //} + + // If change tracking is explicit or happens through notification, then only compute + // changes on entities of that type that are explicitly marked for synchronization. + $entitiesToProcess = ! $class->isChangeTrackingDeferredImplicit() ? + (isset($this->scheduledForDirtyCheck[$className]) ? + $this->scheduledForDirtyCheck[$className] : array()) + : $entities; + + foreach ($entitiesToProcess as $entity) { + // Ignore uninitialized proxy objects + if (/* $entity is readOnly || */ $entity instanceof Proxy && ! $entity->__isInitialized__) { + continue; + } + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. + $oid = spl_object_hash($entity); + if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { + $this->computeChangeSet($class, $entity); + } + } + } + } + + /** + * Computes the changes of an association. + * + * @param AssociationMapping $assoc + * @param mixed $value The value of the association. + */ + private function computeAssociationChanges($assoc, $value) + { + if ($value instanceof PersistentCollection && $value->isDirty()) { + if ($assoc['isOwningSide']) { + $this->collectionUpdates[] = $value; + } + $this->visitedCollections[] = $value; + } + + // Look through the entities, and in any of their associations, for transient (new) + // enities, recursively. ("Persistence by reachability") + if ($assoc['type'] & ClassMetadata::TO_ONE) { + if ($value instanceof Proxy && ! $value->__isInitialized__) { + return; // Ignore uninitialized proxy objects + } + $value = array($value); + } else if ($value instanceof PersistentCollection) { + // Unwrap. Uninitialized collections will simply be empty. + $value = $value->unwrap(); + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + foreach ($value as $entry) { + $state = $this->getEntityState($entry, self::STATE_NEW); + $oid = spl_object_hash($entry); + if ($state == self::STATE_NEW) { + if ( ! $assoc['isCascadePersist']) { + throw new InvalidArgumentException("A new entity was found through a relationship that was not" + . " configured to cascade persist operations: " . self::objToStr($entry) . "." + . " Explicitly persist the new entity or configure cascading persist operations" + . " on the relationship."); + } + $this->persistNew($targetClass, $entry); + $this->computeChangeSet($targetClass, $entry); + } else if ($state == self::STATE_REMOVED) { + return new InvalidArgumentException("Removed entity detected during flush: " + . self::objToStr($entry).". Remove deleted entities from associations."); + } else if ($state == self::STATE_DETACHED) { + // Can actually not happen right now as we assume STATE_NEW, + // so the exception will be raised from the DBAL layer (constraint violation). + throw new InvalidArgumentException("A detached entity was found through a " + . "relationship during cascading a persist operation."); + } + // MANAGED associated entities are already taken into account + // during changeset calculation anyway, since they are in the identity map. + } + } + + private function persistNew($class, $entity) + { + $oid = spl_object_hash($entity); + if (isset($class->lifecycleCallbacks[Events::prePersist])) { + $class->invokeLifecycleCallbacks(Events::prePersist, $entity); + } + if ($this->evm->hasListeners(Events::prePersist)) { + $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->em)); + } + + $idGen = $class->idGenerator; + if ( ! $idGen->isPostInsertGenerator()) { + $idValue = $idGen->generate($this->em, $entity); + if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { + $this->entityIdentifiers[$oid] = array($class->identifier[0] => $idValue); + $class->setIdentifierValues($entity, $this->entityIdentifiers[$oid]); + } else { + $this->entityIdentifiers[$oid] = $idValue; + } + } + $this->entityStates[$oid] = self::STATE_MANAGED; + + $this->scheduleForInsert($entity); + } + + /** + * INTERNAL: + * Computes the changeset of an individual entity, independently of the + * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). + * + * The passed entity must be a managed entity. If the entity already has a change set + * because this method is invoked during a commit cycle then the change sets are added. + * whereby changes detected in this method prevail. + * + * @ignore + * @param ClassMetadata $class The class descriptor of the entity. + * @param object $entity The entity for which to (re)calculate the change set. + * @throws InvalidArgumentException If the passed entity is not MANAGED. + */ + public function recomputeSingleEntityChangeSet($class, $entity) + { + $oid = spl_object_hash($entity); + + if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { + throw new InvalidArgumentException('Entity must be managed.'); + } + + /* TODO: Just return if changetracking policy is NOTIFY? + if ($class->isChangeTrackingNotify()) { + return; + }*/ + + if ( ! $class->isInheritanceTypeNone()) { + $class = $this->em->getClassMetadata(get_class($entity)); + } + + $actualData = array(); + foreach ($class->reflFields as $name => $refProp) { + if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) { + $actualData[$name] = $refProp->getValue($entity); + } + } + + $originalData = $this->originalEntityData[$oid]; + $changeSet = array(); + + foreach ($actualData as $propName => $actualValue) { + $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; + if (is_object($orgValue) && $orgValue !== $actualValue) { + $changeSet[$propName] = array($orgValue, $actualValue); + } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) { + $changeSet[$propName] = array($orgValue, $actualValue); + } + } + + if ($changeSet) { + if (isset($this->entityChangeSets[$oid])) { + $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); + } + $this->originalEntityData[$oid] = $actualData; + } + } + + /** + * Executes all entity insertions for entities of the specified type. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + */ + private function executeInserts($class) + { + $className = $class->name; + $persister = $this->getEntityPersister($className); + + $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]); + $hasListeners = $this->evm->hasListeners(Events::postPersist); + if ($hasLifecycleCallbacks || $hasListeners) { + $entities = array(); + } + + foreach ($this->entityInsertions as $oid => $entity) { + if (get_class($entity) === $className) { + $persister->addInsert($entity); + unset($this->entityInsertions[$oid]); + if ($hasLifecycleCallbacks || $hasListeners) { + $entities[] = $entity; + } + } + } + + $postInsertIds = $persister->executeInserts(); + + if ($postInsertIds) { + // Persister returned post-insert IDs + foreach ($postInsertIds as $id => $entity) { + $oid = spl_object_hash($entity); + $idField = $class->identifier[0]; + $class->reflFields[$idField]->setValue($entity, $id); + $this->entityIdentifiers[$oid] = array($idField => $id); + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->originalEntityData[$oid][$idField] = $id; + $this->addToIdentityMap($entity); + } + } + + if ($hasLifecycleCallbacks || $hasListeners) { + foreach ($entities as $entity) { + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postPersist, $entity); + } + if ($hasListeners) { + $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em)); + } + } + } + } + + /** + * Executes all entity updates for entities of the specified type. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + */ + private function executeUpdates($class) + { + $className = $class->name; + $persister = $this->getEntityPersister($className); + + $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]); + $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate); + $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]); + $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate); + + foreach ($this->entityUpdates as $oid => $entity) { + if (get_class($entity) == $className || $entity instanceof Proxy && $entity instanceof $className) { + + if ($hasPreUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::preUpdate, $entity); + $this->recomputeSingleEntityChangeSet($class, $entity); + } + + if ($hasPreUpdateListeners) { + $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs( + $entity, $this->em, $this->entityChangeSets[$oid]) + ); + } + + $persister->update($entity); + unset($this->entityUpdates[$oid]); + + if ($hasPostUpdateLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postUpdate, $entity); + } + if ($hasPostUpdateListeners) { + $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em)); + } + } + } + } + + /** + * Executes all entity deletions for entities of the specified type. + * + * @param Doctrine\ORM\Mapping\ClassMetadata $class + */ + private function executeDeletions($class) + { + $className = $class->name; + $persister = $this->getEntityPersister($className); + + $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]); + $hasListeners = $this->evm->hasListeners(Events::postRemove); + + foreach ($this->entityDeletions as $oid => $entity) { + if (get_class($entity) == $className || $entity instanceof Proxy && $entity instanceof $className) { + $persister->delete($entity); + unset( + $this->entityDeletions[$oid], + $this->entityIdentifiers[$oid], + $this->originalEntityData[$oid], + $this->entityStates[$oid] + ); + // Entity with this $oid after deletion treated as NEW, even if the $oid + // is obtained by a new entity because the old one went out of scope. + //$this->entityStates[$oid] = self::STATE_NEW; + if ( ! $class->isIdentifierNatural()) { + $class->reflFields[$class->identifier[0]]->setValue($entity, null); + } + + if ($hasLifecycleCallbacks) { + $class->invokeLifecycleCallbacks(Events::postRemove, $entity); + } + if ($hasListeners) { + $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em)); + } + } + } + } + + /** + * Gets the commit order. + * + * @return array + */ + private function getCommitOrder(array $entityChangeSet = null) + { + if ($entityChangeSet === null) { + $entityChangeSet = array_merge( + $this->entityInsertions, + $this->entityUpdates, + $this->entityDeletions + ); + } + + $calc = $this->getCommitOrderCalculator(); + + // 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 $oid => $entity) { + $className = get_class($entity); + if ( ! $calc->hasClass($className)) { + $class = $this->em->getClassMetadata($className); + $calc->addClass($class); + $newNodes[] = $class; + } + } + + // Calculate dependencies for new nodes + foreach ($newNodes as $class) { + foreach ($class->associationMappings as $assoc) { + if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + if ( ! $calc->hasClass($targetClass->name)) { + $calc->addClass($targetClass); + } + $calc->addDependency($targetClass, $class); + // If the target class has mapped subclasses, + // these share the same dependency. + if ($targetClass->subClasses) { + foreach ($targetClass->subClasses as $subClassName) { + $targetSubClass = $this->em->getClassMetadata($subClassName); + if ( ! $calc->hasClass($subClassName)) { + $calc->addClass($targetSubClass); + } + $calc->addDependency($targetSubClass, $class); + } + } + } + } + } + + return $calc->getCommitOrder(); + } + + /** + * Schedules an entity for insertion into the database. + * If the entity already has an identifier, it will be added to the identity map. + * + * @param object $entity The entity to schedule for insertion. + */ + public function scheduleForInsert($entity) + { + $oid = spl_object_hash($entity); + + if (isset($this->entityUpdates[$oid])) { + throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion."); + } + if (isset($this->entityDeletions[$oid])) { + throw new InvalidArgumentException("Removed entity can not be scheduled for insertion."); + } + if (isset($this->entityInsertions[$oid])) { + throw new InvalidArgumentException("Entity can not be scheduled for insertion twice."); + } + + $this->entityInsertions[$oid] = $entity; + + if (isset($this->entityIdentifiers[$oid])) { + $this->addToIdentityMap($entity); + } + } + + /** + * Checks whether an entity is scheduled for insertion. + * + * @param object $entity + * @return boolean + */ + public function isScheduledForInsert($entity) + { + return isset($this->entityInsertions[spl_object_hash($entity)]); + } + + /** + * Schedules an entity for being updated. + * + * @param object $entity The entity to schedule for being updated. + */ + public function scheduleForUpdate($entity) + { + $oid = spl_object_hash($entity); + if ( ! isset($this->entityIdentifiers[$oid])) { + throw new InvalidArgumentException("Entity has no identity."); + } + if (isset($this->entityDeletions[$oid])) { + throw new InvalidArgumentException("Entity is removed."); + } + + if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { + $this->entityUpdates[$oid] = $entity; + } + } + + /** + * INTERNAL: + * Schedules an extra update that will be executed immediately after the + * regular entity updates within the currently running commit cycle. + * + * Extra updates for entities are stored as (entity, changeset) tuples. + * + * @ignore + * @param object $entity The entity for which to schedule an extra update. + * @param array $changeset The changeset of the entity (what to update). + */ + public function scheduleExtraUpdate($entity, array $changeset) + { + $oid = spl_object_hash($entity); + if (isset($this->extraUpdates[$oid])) { + list($ignored, $changeset2) = $this->extraUpdates[$oid]; + $this->extraUpdates[$oid] = array($entity, $changeset + $changeset2); + } else { + $this->extraUpdates[$oid] = array($entity, $changeset); + } + } + + /** + * 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 object $entity + * @return boolean + */ + public function isScheduledForUpdate($entity) + { + return isset($this->entityUpdates[spl_object_hash($entity)]); + } + + public function isScheduledForDirtyCheck($entity) + { + $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; + return isset($this->scheduledForDirtyCheck[$rootEntityName][spl_object_hash($entity)]); + } + + /** + * INTERNAL: + * Schedules an entity for deletion. + * + * @param object $entity + */ + public function scheduleForDelete($entity) + { + $oid = spl_object_hash($entity); + + if (isset($this->entityInsertions[$oid])) { + if ($this->isInIdentityMap($entity)) { + $this->removeFromIdentityMap($entity); + } + unset($this->entityInsertions[$oid]); + return; // entity has not been persisted yet, so nothing more to do. + } + + if ( ! $this->isInIdentityMap($entity)) { + return; // ignore + } + + $this->removeFromIdentityMap($entity); + + if (isset($this->entityUpdates[$oid])) { + unset($this->entityUpdates[$oid]); + } + if ( ! isset($this->entityDeletions[$oid])) { + $this->entityDeletions[$oid] = $entity; + } + } + + /** + * Checks whether an entity is registered as removed/deleted with the unit + * of work. + * + * @param object $entity + * @return boolean + */ + public function isScheduledForDelete($entity) + { + return isset($this->entityDeletions[spl_object_hash($entity)]); + } + + /** + * Checks whether an entity is scheduled for insertion, update or deletion. + * + * @param $entity + * @return boolean + */ + public function isEntityScheduled($entity) + { + $oid = spl_object_hash($entity); + return isset($this->entityInsertions[$oid]) || + isset($this->entityUpdates[$oid]) || + isset($this->entityDeletions[$oid]); + } + + /** + * INTERNAL: + * Registers an entity in the identity map. + * Note that entities in a hierarchy are registered with the class name of + * the root entity. + * + * @ignore + * @param object $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. + */ + public function addToIdentityMap($entity) + { + $classMetadata = $this->em->getClassMetadata(get_class($entity)); + $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]); + if ($idHash === '') { + throw new InvalidArgumentException("The given entity has no identity."); + } + $className = $classMetadata->rootEntityName; + if (isset($this->identityMap[$className][$idHash])) { + return false; + } + $this->identityMap[$className][$idHash] = $entity; + if ($entity instanceof NotifyPropertyChanged) { + $entity->addPropertyChangedListener($this); + } + return true; + } + + /** + * Gets the state of an entity with regard to the current unit of work. + * + * @param object $entity + * @param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). + * This parameter can be set to improve performance of entity state detection + * by potentially avoiding a database lookup if the distinction between NEW and DETACHED + * is either known or does not matter for the caller of the method. + * @return int The entity state. + */ + public function getEntityState($entity, $assume = null) + { + $oid = spl_object_hash($entity); + if ( ! isset($this->entityStates[$oid])) { + // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known. + // Note that you can not remember the NEW or DETACHED state in _entityStates since + // the UoW does not hold references to such objects and the object hash can be reused. + // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it. + if ($assume === null) { + $class = $this->em->getClassMetadata(get_class($entity)); + $id = $class->getIdentifierValues($entity); + if ( ! $id) { + return self::STATE_NEW; + } else if ($class->isIdentifierNatural()) { + // Check for a version field, if available, to avoid a db lookup. + if ($class->isVersioned) { + if ($class->getFieldValue($entity, $class->versionField)) { + return self::STATE_DETACHED; + } else { + return self::STATE_NEW; + } + } else { + // Last try before db lookup: check the identity map. + if ($this->tryGetById($id, $class->rootEntityName)) { + return self::STATE_DETACHED; + } else { + // db lookup + if ($this->getEntityPersister(get_class($entity))->exists($entity)) { + return self::STATE_DETACHED; + } else { + return self::STATE_NEW; + } + } + } + } else { + return self::STATE_DETACHED; + } + } else { + return $assume; + } + } + return $this->entityStates[$oid]; + } + + /** + * INTERNAL: + * Removes an entity from the identity map. This effectively detaches the + * entity from the persistence management of Doctrine. + * + * @ignore + * @param object $entity + * @return boolean + */ + public function removeFromIdentityMap($entity) + { + $oid = spl_object_hash($entity); + $classMetadata = $this->em->getClassMetadata(get_class($entity)); + $idHash = implode(' ', $this->entityIdentifiers[$oid]); + if ($idHash === '') { + throw new InvalidArgumentException("The given entity has no identity."); + } + $className = $classMetadata->rootEntityName; + if (isset($this->identityMap[$className][$idHash])) { + unset($this->identityMap[$className][$idHash]); + //$this->entityStates[$oid] = self::STATE_DETACHED; + return true; + } + + return false; + } + + /** + * INTERNAL: + * Gets an entity in the identity map by its identifier hash. + * + * @ignore + * @param string $idHash + * @param string $rootClassName + * @return object + */ + public function getByIdHash($idHash, $rootClassName) + { + return $this->identityMap[$rootClassName][$idHash]; + } + + /** + * INTERNAL: + * Tries to get an entity by its identifier hash. If no entity is found for + * the given hash, FALSE is returned. + * + * @ignore + * @param string $idHash + * @param string $rootClassName + * @return mixed The found entity or FALSE. + */ + public function tryGetByIdHash($idHash, $rootClassName) + { + return isset($this->identityMap[$rootClassName][$idHash]) ? + $this->identityMap[$rootClassName][$idHash] : false; + } + + /** + * Checks whether an entity is registered in the identity map of this UnitOfWork. + * + * @param object $entity + * @return boolean + */ + public function isInIdentityMap($entity) + { + $oid = spl_object_hash($entity); + if ( ! isset($this->entityIdentifiers[$oid])) { + return false; + } + $classMetadata = $this->em->getClassMetadata(get_class($entity)); + $idHash = implode(' ', $this->entityIdentifiers[$oid]); + if ($idHash === '') { + return false; + } + + return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]); + } + + /** + * INTERNAL: + * Checks whether an identifier hash exists in the identity map. + * + * @ignore + * @param string $idHash + * @param string $rootClassName + * @return boolean + */ + public function containsIdHash($idHash, $rootClassName) + { + return isset($this->identityMap[$rootClassName][$idHash]); + } + + /** + * Persists an entity as part of the current unit of work. + * + * @param object $entity The entity to persist. + */ + public function persist($entity) + { + $visited = array(); + $this->doPersist($entity, $visited); + } + + /** + * Persists an entity as part of the current unit of work. + * + * This method is internally called during persist() cascades as it tracks + * the already visited entities to prevent infinite recursions. + * + * @param object $entity The entity to persist. + * @param array $visited The already visited entities. + */ + private function doPersist($entity, array &$visited) + { + $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // Mark visited + + $class = $this->em->getClassMetadata(get_class($entity)); + + // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation). + // If we would detect DETACHED here we would throw an exception anyway with the same + // consequences (not recoverable/programming error), so just assuming NEW here + // lets us avoid some database lookups for entities with natural identifiers. + $entityState = $this->getEntityState($entity, self::STATE_NEW); + + switch ($entityState) { + case self::STATE_MANAGED: + // Nothing to do, except if policy is "deferred explicit" + if ($class->isChangeTrackingDeferredExplicit()) { + $this->scheduleForDirtyCheck($entity); + } + break; + case self::STATE_NEW: + $this->persistNew($class, $entity); + break; + case self::STATE_REMOVED: + // Entity becomes managed again + unset($this->entityDeletions[$oid]); + $this->entityStates[$oid] = self::STATE_MANAGED; + break; + case self::STATE_DETACHED: + // Can actually not happen right now since we assume STATE_NEW. + throw new InvalidArgumentException("Detached entity passed to persist()."); + default: + throw new UnexpectedValueException("Unexpected entity state: $entityState."); + } + + $this->cascadePersist($entity, $visited); + } + + /** + * Deletes an entity as part of the current unit of work. + * + * @param object $entity The entity to remove. + */ + public function remove($entity) + { + $visited = array(); + $this->doRemove($entity, $visited); + } + + /** + * Deletes an entity as part of the current unit of work. + * + * This method is internally called during delete() cascades as it tracks + * the already visited entities to prevent infinite recursions. + * + * @param object $entity The entity to delete. + * @param array $visited The map of the already visited entities. + * @throws InvalidArgumentException If the instance is a detached entity. + */ + private function doRemove($entity, array &$visited) + { + $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + $class = $this->em->getClassMetadata(get_class($entity)); + $entityState = $this->getEntityState($entity); + switch ($entityState) { + case self::STATE_NEW: + case self::STATE_REMOVED: + // nothing to do + break; + case self::STATE_MANAGED: + if (isset($class->lifecycleCallbacks[Events::preRemove])) { + $class->invokeLifecycleCallbacks(Events::preRemove, $entity); + } + if ($this->evm->hasListeners(Events::preRemove)) { + $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity, $this->em)); + } + $this->scheduleForDelete($entity); + break; + case self::STATE_DETACHED: + throw new InvalidArgumentException("A detached entity can not be removed."); + default: + throw new UnexpectedValueException("Unexpected entity state: $entityState."); + } + + $this->cascadeRemove($entity, $visited); + } + + /** + * Merges the state of the given detached entity into this UnitOfWork. + * + * @param object $entity + * @return object The managed copy of the entity. + * @throws OptimisticLockException If the entity uses optimistic locking through a version + * attribute and the version check against the managed copy fails. + * + * @todo Require active transaction!? OptimisticLockException may result in undefined state!? + */ + public function merge($entity) + { + $visited = array(); + return $this->doMerge($entity, $visited); + } + + /** + * Executes a merge operation on an entity. + * + * @param object $entity + * @param array $visited + * @return object The managed copy of the entity. + * @throws OptimisticLockException If the entity uses optimistic locking through a version + * attribute and the version check against the managed copy fails. + * @throws InvalidArgumentException If the entity instance is NEW. + */ + private function doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null) + { + $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + $class = $this->em->getClassMetadata(get_class($entity)); + + // First we assume DETACHED, although it can still be NEW but we can avoid + // an extra db-roundtrip this way. If it is not MANAGED but has an identity, + // we need to fetch it from the db anyway in order to merge. + // MANAGED entities are ignored by the merge operation. + if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) { + $managedCopy = $entity; + } else { + // Try to look the entity up in the identity map. + $id = $class->getIdentifierValues($entity); + + // If there is no ID, it is actually NEW. + if ( ! $id) { + $managedCopy = $class->newInstance(); + $this->persistNew($class, $managedCopy); + } else { + $managedCopy = $this->tryGetById($id, $class->rootEntityName); + if ($managedCopy) { + // We have the entity in-memory already, just make sure its not removed. + if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) { + throw new InvalidArgumentException('Removed entity detected during merge.' + . ' Can not merge with a removed entity.'); + } + } else { + // We need to fetch the managed copy in order to merge. + $managedCopy = $this->em->find($class->name, $id); + } + + if ($managedCopy === null) { + // If the identifier is ASSIGNED, it is NEW, otherwise an error + // since the managed entity was not found. + if ($class->isIdentifierNatural()) { + $managedCopy = $class->newInstance(); + $class->setIdentifierValues($managedCopy, $id); + $this->persistNew($class, $managedCopy); + } else { + throw new EntityNotFoundException; + } + } + } + + if ($class->isVersioned) { + $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy); + $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + // Throw exception if versions dont match. + if ($managedCopyVersion != $entityVersion) { + throw OptimisticLockException::lockFailedVersionMissmatch($entityVersion, $managedCopyVersion); + } + } + + // Merge state of $entity into existing (managed) entity + foreach ($class->reflFields as $name => $prop) { + if ( ! isset($class->associationMappings[$name])) { + if ( ! $class->isIdentifier($name)) { + $prop->setValue($managedCopy, $prop->getValue($entity)); + } + } else { + $assoc2 = $class->associationMappings[$name]; + if ($assoc2['type'] & ClassMetadata::TO_ONE) { + $other = $prop->getValue($entity); + if ($other === null) { + $prop->setValue($managedCopy, null); + } else if ($other instanceof Proxy && !$other->__isInitialized__) { + // do not merge fields marked lazy that have not been fetched. + continue; + } else if ( ! $assoc2['isCascadeMerge']) { + if ($this->getEntityState($other, self::STATE_DETACHED) == self::STATE_MANAGED) { + $prop->setValue($managedCopy, $other); + } else { + $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); + $id = $targetClass->getIdentifierValues($other); + $proxy = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $id); + $prop->setValue($managedCopy, $proxy); + $this->registerManaged($proxy, $id, array()); + } + } + } else { + $mergeCol = $prop->getValue($entity); + if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) { + // do not merge fields marked lazy that have not been fetched. + // keep the lazy persistent collection of the managed copy. + continue; + } + + $managedCol = $prop->getValue($managedCopy); + if (!$managedCol) { + $managedCol = new PersistentCollection($this->em, + $this->em->getClassMetadata($assoc2['targetEntity']), + new ArrayCollection + ); + $managedCol->setOwner($managedCopy, $assoc2); + $prop->setValue($managedCopy, $managedCol); + $this->originalEntityData[$oid][$name] = $managedCol; + } + if ($assoc2['isCascadeMerge']) { + $managedCol->initialize(); + if (!$managedCol->isEmpty()) { + $managedCol->unwrap()->clear(); + $managedCol->setDirty(true); + if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) { + $this->scheduleForDirtyCheck($managedCopy); + } + } + } + } + } + if ($class->isChangeTrackingNotify()) { + // Just treat all properties as changed, there is no other choice. + $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy)); + } + } + if ($class->isChangeTrackingDeferredExplicit()) { + $this->scheduleForDirtyCheck($entity); + } + } + + if ($prevManagedCopy !== null) { + $assocField = $assoc['fieldName']; + $prevClass = $this->em->getClassMetadata(get_class($prevManagedCopy)); + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy); + } else { + $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy); + if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) { + $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy); + } + } + } + + // Mark the managed copy visited as well + $visited[spl_object_hash($managedCopy)] = true; + + $this->cascadeMerge($entity, $managedCopy, $visited); + + return $managedCopy; + } + + /** + * Detaches an entity from the persistence management. It's persistence will + * no longer be managed by Doctrine. + * + * @param object $entity The entity to detach. + */ + public function detach($entity) + { + $visited = array(); + $this->doDetach($entity, $visited); + } + + /** + * Executes a detach operation on the given entity. + * + * @param object $entity + * @param array $visited + */ + private function doDetach($entity, array &$visited) + { + $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + switch ($this->getEntityState($entity, self::STATE_DETACHED)) { + case self::STATE_MANAGED: + $this->removeFromIdentityMap($entity); + unset($this->entityInsertions[$oid], $this->entityUpdates[$oid], + $this->entityDeletions[$oid], $this->entityIdentifiers[$oid], + $this->entityStates[$oid], $this->originalEntityData[$oid]); + break; + case self::STATE_NEW: + case self::STATE_DETACHED: + return; + } + + $this->cascadeDetach($entity, $visited); + } + + /** + * Refreshes the state of the given entity from the database, overwriting + * any local, unpersisted changes. + * + * @param object $entity The entity to refresh. + * @throws InvalidArgumentException If the entity is not MANAGED. + */ + public function refresh($entity) + { + $visited = array(); + $this->doRefresh($entity, $visited); + } + + /** + * Executes a refresh operation on an entity. + * + * @param object $entity The entity to refresh. + * @param array $visited The already visited entities during cascades. + * @throws InvalidArgumentException If the entity is not MANAGED. + */ + private function doRefresh($entity, array &$visited) + { + $oid = spl_object_hash($entity); + if (isset($visited[$oid])) { + return; // Prevent infinite recursion + } + + $visited[$oid] = $entity; // mark visited + + $class = $this->em->getClassMetadata(get_class($entity)); + if ($this->getEntityState($entity) == self::STATE_MANAGED) { + $this->getEntityPersister($class->name)->refresh( + array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), + $entity + ); + } else { + throw new InvalidArgumentException("Entity is not MANAGED."); + } + + $this->cascadeRefresh($entity, $visited); + } + + /** + * Cascades a refresh operation to associated entities. + * + * @param object $entity + * @param array $visited + */ + private function cascadeRefresh($entity, array &$visited) + { + $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { + if ( ! $assoc['isCascadeRefresh']) { + continue; + } + $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if ($relatedEntities instanceof Collection) { + if ($relatedEntities instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + } + foreach ($relatedEntities as $relatedEntity) { + $this->doRefresh($relatedEntity, $visited); + } + } else if ($relatedEntities !== null) { + $this->doRefresh($relatedEntities, $visited); + } + } + } + + /** + * Cascades a detach operation to associated entities. + * + * @param object $entity + * @param array $visited + */ + private function cascadeDetach($entity, array &$visited) + { + $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { + if ( ! $assoc['isCascadeDetach']) { + continue; + } + $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if ($relatedEntities instanceof Collection) { + if ($relatedEntities instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + } + foreach ($relatedEntities as $relatedEntity) { + $this->doDetach($relatedEntity, $visited); + } + } else if ($relatedEntities !== null) { + $this->doDetach($relatedEntities, $visited); + } + } + } + + /** + * Cascades a merge operation to associated entities. + * + * @param object $entity + * @param object $managedCopy + * @param array $visited + */ + private function cascadeMerge($entity, $managedCopy, array &$visited) + { + $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { + if ( ! $assoc['isCascadeMerge']) { + continue; + } + $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if ($relatedEntities instanceof Collection) { + if ($relatedEntities instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + } + foreach ($relatedEntities as $relatedEntity) { + $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc); + } + } else if ($relatedEntities !== null) { + $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc); + } + } + } + + /** + * Cascades the save operation to associated entities. + * + * @param object $entity + * @param array $visited + * @param array $insertNow + */ + private function cascadePersist($entity, array &$visited) + { + $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { + if ( ! $assoc['isCascadePersist']) { + continue; + } + $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if (($relatedEntities instanceof Collection || is_array($relatedEntities))) { + if ($relatedEntities instanceof PersistentCollection) { + // Unwrap so that foreach() does not initialize + $relatedEntities = $relatedEntities->unwrap(); + } + foreach ($relatedEntities as $relatedEntity) { + $this->doPersist($relatedEntity, $visited); + } + } else if ($relatedEntities !== null) { + $this->doPersist($relatedEntities, $visited); + } + } + } + + /** + * Cascades the delete operation to associated entities. + * + * @param object $entity + * @param array $visited + */ + private function cascadeRemove($entity, array &$visited) + { + $class = $this->em->getClassMetadata(get_class($entity)); + foreach ($class->associationMappings as $assoc) { + if ( ! $assoc['isCascadeRemove']) { + continue; + } + //TODO: If $entity instanceof Proxy => Initialize ? + $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity); + if ($relatedEntities instanceof Collection || is_array($relatedEntities)) { + // If its a PersistentCollection initialization is intended! No unwrap! + foreach ($relatedEntities as $relatedEntity) { + $this->doRemove($relatedEntity, $visited); + } + } else if ($relatedEntities !== null) { + $this->doRemove($relatedEntities, $visited); + } + } + } + + /** + * Acquire a lock on the given entity. + * + * @param object $entity + * @param int $lockMode + * @param int $lockVersion + */ + public function lock($entity, $lockMode, $lockVersion = null) + { + if ($this->getEntityState($entity) != self::STATE_MANAGED) { + throw new InvalidArgumentException("Entity is not MANAGED."); + } + + $entityName = get_class($entity); + $class = $this->em->getClassMetadata($entityName); + + if ($lockMode == \Doctrine\DBAL\LockMode::OPTIMISTIC) { + if (!$class->isVersioned) { + throw OptimisticLockException::notVersioned($entityName); + } + + if ($lockVersion != null) { + $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); + if ($entityVersion != $lockVersion) { + throw OptimisticLockException::lockFailedVersionMissmatch($entity, $lockVersion, $entityVersion); + } + } + } else if (in_array($lockMode, array(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE))) { + + if (!$this->em->getConnection()->isTransactionActive()) { + throw TransactionRequiredException::transactionRequired(); + } + + $oid = spl_object_hash($entity); + + $this->getEntityPersister($class->name)->lock( + array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), + $lockMode + ); + } + } + + /** + * Gets the CommitOrderCalculator used by the UnitOfWork to order commits. + * + * @return Doctrine\ORM\Internal\CommitOrderCalculator + */ + public function getCommitOrderCalculator() + { + if ($this->commitOrderCalculator === null) { + $this->commitOrderCalculator = new Internal\CommitOrderCalculator; + } + return $this->commitOrderCalculator; + } + + /** + * Clears the UnitOfWork. + */ + public function clear() + { + $this->identityMap = + $this->entityIdentifiers = + $this->originalEntityData = + $this->entityChangeSets = + $this->entityStates = + $this->scheduledForDirtyCheck = + $this->entityInsertions = + $this->entityUpdates = + $this->entityDeletions = + $this->collectionDeletions = + $this->collectionUpdates = + $this->extraUpdates = + $this->orphanRemovals = array(); + if ($this->commitOrderCalculator !== null) { + $this->commitOrderCalculator->clear(); + } + } + + /** + * INTERNAL: + * Schedules an orphaned entity for removal. The remove() operation will be + * invoked on that entity at the beginning of the next commit of this + * UnitOfWork. + * + * @ignore + * @param object $entity + */ + public function scheduleOrphanRemoval($entity) + { + $this->orphanRemovals[spl_object_hash($entity)] = $entity; + } + + /** + * INTERNAL: + * Schedules a complete collection for removal when this UnitOfWork commits. + * + * @param PersistentCollection $coll + */ + public function scheduleCollectionDeletion(PersistentCollection $coll) + { + //TODO: if $coll is already scheduled for recreation ... what to do? + // Just remove $coll from the scheduled recreations? + $this->collectionDeletions[] = $coll; + } + + public function isCollectionScheduledForDeletion(PersistentCollection $coll) + { + return in_array($coll, $this->collectionsDeletions, true); + } + + /** + * INTERNAL: + * Creates an entity. Used for reconstitution of persistent entities. + * + * @ignore + * @param string $className The name of the entity class. + * @param array $data The data for the entity. + * @param array $hints Any hints to account for during reconstitution/lookup of the entity. + * @return object The managed entity instance. + * @internal Highly performance-sensitive method. + * + * @todo Rename: getOrCreateEntity + */ + public function createEntity($className, array $data, &$hints = array()) + { + $class = $this->em->getClassMetadata($className); + //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]); + + if ($class->isIdentifierComposite) { + $id = array(); + foreach ($class->identifier as $fieldName) { + $id[$fieldName] = $data[$fieldName]; + } + $idHash = implode(' ', $id); + } else { + $idHash = $data[$class->identifier[0]]; + $id = array($class->identifier[0] => $idHash); + } + + if (isset($this->identityMap[$class->rootEntityName][$idHash])) { + $entity = $this->identityMap[$class->rootEntityName][$idHash]; + $oid = spl_object_hash($entity); + if ($entity instanceof Proxy && ! $entity->__isInitialized__) { + $entity->__isInitialized__ = true; + $overrideLocalValues = true; + $this->originalEntityData[$oid] = $data; + if ($entity instanceof NotifyPropertyChanged) { + $entity->addPropertyChangedListener($this); + } + } else { + $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); + } + } else { + $entity = $class->newInstance(); + $oid = spl_object_hash($entity); + $this->entityIdentifiers[$oid] = $id; + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->originalEntityData[$oid] = $data; + $this->identityMap[$class->rootEntityName][$idHash] = $entity; + if ($entity instanceof NotifyPropertyChanged) { + $entity->addPropertyChangedListener($this); + } + $overrideLocalValues = true; + } + + if ($overrideLocalValues) { + foreach ($data as $field => $value) { + if (isset($class->fieldMappings[$field])) { + $class->reflFields[$field]->setValue($entity, $value); + } + } + + // Properly initialize any unfetched associations, if partial objects are not allowed. + if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) { + foreach ($class->associationMappings as $field => $assoc) { + // Check if the association is not among the fetch-joined associations already. + if (isset($hints['fetched'][$className][$field])) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + if ($assoc['isOwningSide']) { + $associatedId = array(); + foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { + $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; + if ($joinColumnValue !== null) { + $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; + } + } + if ( ! $associatedId) { + // Foreign key is NULL + $class->reflFields[$field]->setValue($entity, null); + $this->originalEntityData[$oid][$field] = null; + } else { + // Foreign key is set + // Check identity map first + // FIXME: Can break easily with composite keys if join column values are in + // wrong order. The correct order is the one in ClassMetadata#identifier. + $relatedIdHash = implode(' ', $associatedId); + if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) { + $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash]; + } else { + if ($targetClass->subClasses) { + // If it might be a subtype, it can not be lazy + $newValue = $this->getEntityPersister($assoc['targetEntity']) + ->loadOneToOneEntity($assoc, $entity, null, $associatedId); + } else { + if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) { + // TODO: Maybe it could be optimized to do an eager fetch with a JOIN inside + // the persister instead of this rather unperformant approach. + $newValue = $this->em->find($assoc['targetEntity'], $associatedId); + } else { + $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId); + } + // PERF: Inlined & optimized code from UnitOfWork#registerManaged() + $newValueOid = spl_object_hash($newValue); + $this->entityIdentifiers[$newValueOid] = $associatedId; + $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue; + $this->entityStates[$newValueOid] = self::STATE_MANAGED; + // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also! + } + } + $this->originalEntityData[$oid][$field] = $newValue; + $class->reflFields[$field]->setValue($entity, $newValue); + } + } else { + // Inverse side of x-to-one can never be lazy + $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity']) + ->loadOneToOneEntity($assoc, $entity, null)); + } + } else { + // Inject collection + $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); + $pColl->setOwner($entity, $assoc); + + $reflField = $class->reflFields[$field]; + $reflField->setValue($entity, $pColl); + + if ($assoc['fetch'] == ClassMetadata::FETCH_LAZY) { + $pColl->setInitialized(false); + } else { + $this->loadCollection($pColl); + $pColl->takeSnapshot(); + } + $this->originalEntityData[$oid][$field] = $pColl; + } + } + } + } + + //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here. + if (isset($class->lifecycleCallbacks[Events::postLoad])) { + $class->invokeLifecycleCallbacks(Events::postLoad, $entity); + } + if ($this->evm->hasListeners(Events::postLoad)) { + $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em)); + } + + return $entity; + } + + /** + * Initializes (loads) an uninitialized persistent collection of an entity. + * + * @param PeristentCollection $collection The collection to initialize. + * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. + */ + public function loadCollection(PersistentCollection $collection) + { + $assoc = $collection->getMapping(); + switch ($assoc['type']) { + case ClassMetadata::ONE_TO_MANY: + $this->getEntityPersister($assoc['targetEntity'])->loadOneToManyCollection( + $assoc, $collection->getOwner(), $collection); + break; + case ClassMetadata::MANY_TO_MANY: + $this->getEntityPersister($assoc['targetEntity'])->loadManyToManyCollection( + $assoc, $collection->getOwner(), $collection); + break; + } + } + + /** + * Gets the identity map of the UnitOfWork. + * + * @return array + */ + public function getIdentityMap() + { + return $this->identityMap; + } + + /** + * Gets the original data of an entity. The original data is the data that was + * present at the time the entity was reconstituted from the database. + * + * @param object $entity + * @return array + */ + public function getOriginalEntityData($entity) + { + $oid = spl_object_hash($entity); + if (isset($this->originalEntityData[$oid])) { + return $this->originalEntityData[$oid]; + } + return array(); + } + + /** + * @ignore + */ + public function setOriginalEntityData($entity, array $data) + { + $this->originalEntityData[spl_object_hash($entity)] = $data; + } + + /** + * INTERNAL: + * Sets a property value of the original data array of an entity. + * + * @ignore + * @param string $oid + * @param string $property + * @param mixed $value + */ + public function setOriginalEntityProperty($oid, $property, $value) + { + $this->originalEntityData[$oid][$property] = $value; + } + + /** + * Gets the identifier of an entity. + * The returned value is always an array of identifier values. If the entity + * has a composite identifier then the identifier values are in the same + * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). + * + * @param object $entity + * @return array The identifier values. + */ + public function getEntityIdentifier($entity) + { + return $this->entityIdentifiers[spl_object_hash($entity)]; + } + + /** + * Tries to find an entity with the given identifier in the identity map of + * this UnitOfWork. + * + * @param mixed $id The entity identifier to look for. + * @param string $rootClassName The name of the root class of the mapped entity hierarchy. + * @return mixed Returns the entity with the specified identifier if it exists in + * this UnitOfWork, FALSE otherwise. + */ + public function tryGetById($id, $rootClassName) + { + $idHash = implode(' ', (array) $id); + if (isset($this->identityMap[$rootClassName][$idHash])) { + return $this->identityMap[$rootClassName][$idHash]; + } + return false; + } + + /** + * Schedules an entity for dirty-checking at commit-time. + * + * @param object $entity The entity to schedule for dirty-checking. + * @todo Rename: scheduleForSynchronization + */ + public function scheduleForDirtyCheck($entity) + { + $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; + $this->scheduledForDirtyCheck[$rootClassName][spl_object_hash($entity)] = $entity; + } + + /** + * Checks whether the UnitOfWork has any pending insertions. + * + * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise. + */ + public function hasPendingInsertions() + { + return ! empty($this->entityInsertions); + } + + /** + * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the + * number of entities in the identity map. + * + * @return integer + */ + public function size() + { + $count = 0; + foreach ($this->identityMap as $entitySet) { + $count += count($entitySet); + } + return $count; + } + + /** + * Gets the EntityPersister for an Entity. + * + * @param string $entityName The name of the Entity. + * @return Doctrine\ORM\Persister\AbstractEntityPersister + */ + public function getEntityPersister($entityName) + { + if ( ! isset($this->persisters[$entityName])) { + $class = $this->em->getClassMetadata($entityName); + if ($class->isInheritanceTypeNone()) { + $persister = new Persisters\BasicEntityPersister($this->em, $class); + } else if ($class->isInheritanceTypeSingleTable()) { + $persister = new Persisters\SingleTablePersister($this->em, $class); + } else if ($class->isInheritanceTypeJoined()) { + $persister = new Persisters\JoinedSubclassPersister($this->em, $class); + } else { + $persister = new Persisters\UnionSubclassPersister($this->em, $class); + } + $this->persisters[$entityName] = $persister; + } + return $this->persisters[$entityName]; + } + + /** + * Gets a collection persister for a collection-valued association. + * + * @param AssociationMapping $association + * @return AbstractCollectionPersister + */ + public function getCollectionPersister(array $association) + { + $type = $association['type']; + if ( ! isset($this->collectionPersisters[$type])) { + if ($type == ClassMetadata::ONE_TO_MANY) { + $persister = new Persisters\OneToManyPersister($this->em); + } else if ($type == ClassMetadata::MANY_TO_MANY) { + $persister = new Persisters\ManyToManyPersister($this->em); + } + $this->collectionPersisters[$type] = $persister; + } + return $this->collectionPersisters[$type]; + } + + /** + * INTERNAL: + * Registers an entity as managed. + * + * @param object $entity The entity. + * @param array $id The identifier values. + * @param array $data The original entity data. + */ + public function registerManaged($entity, array $id, array $data) + { + $oid = spl_object_hash($entity); + $this->entityIdentifiers[$oid] = $id; + $this->entityStates[$oid] = self::STATE_MANAGED; + $this->originalEntityData[$oid] = $data; + $this->addToIdentityMap($entity); + } + + /** + * INTERNAL: + * Clears the property changeset of the entity with the given OID. + * + * @param string $oid The entity's OID. + */ + public function clearEntityChangeSet($oid) + { + unset($this->entityChangeSets[$oid]); + } + + /* PropertyChangedListener implementation */ + + /** + * Notifies this UnitOfWork of a property change in an entity. + * + * @param object $entity The entity that owns the property. + * @param string $propertyName The name of the property that changed. + * @param mixed $oldValue The old value of the property. + * @param mixed $newValue The new value of the property. + */ + public function propertyChanged($entity, $propertyName, $oldValue, $newValue) + { + $oid = spl_object_hash($entity); + $class = $this->em->getClassMetadata(get_class($entity)); + + $isAssocField = isset($class->associationMappings[$propertyName]); + + if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) { + return; // ignore non-persistent fields + } + + // Update changeset and mark entity for synchronization + $this->entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue); + if ( ! isset($this->scheduledForDirtyCheck[$class->rootEntityName][$oid])) { + $this->scheduleForDirtyCheck($entity); + } + } + + /** + * Gets the currently scheduled entity insertions in this UnitOfWork. + * + * @return array + */ + public function getScheduledEntityInsertions() + { + return $this->entityInsertions; + } + + /** + * Gets the currently scheduled entity updates in this UnitOfWork. + * + * @return array + */ + public function getScheduledEntityUpdates() + { + return $this->entityUpdates; + } + + /** + * Gets the currently scheduled entity deletions in this UnitOfWork. + * + * @return array + */ + public function getScheduledEntityDeletions() + { + return $this->entityDeletions; + } + + /** + * Get the currently scheduled complete collection deletions + * + * @return array + */ + public function getScheduledCollectionDeletions() + { + return $this->collectionDeletions; + } + + /** + * Gets the currently scheduled collection inserts, updates and deletes. + * + * @return array + */ + public function getScheduledCollectionUpdates() + { + return $this->collectionUpdates; + } + + private static function objToStr($obj) + { + return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj); + } +} diff --git a/Doctrine/ORM/Version.php b/Doctrine/ORM/Version.php new file mode 100644 index 0000000..51e0460 --- /dev/null +++ b/Doctrine/ORM/Version.php @@ -0,0 +1,55 @@ +. + */ + +namespace Doctrine\ORM; + +/** + * Class to store and retrieve the version of Doctrine + * + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @link www.doctrine-project.org + * @since 2.0 + * @version $Revision$ + * @author Benjamin Eberlei + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + */ +class Version +{ + /** + * Current Doctrine Version + */ + const VERSION = '2.0.0RC1'; + + /** + * Compares a Doctrine version with the current one. + * + * @param string $version Doctrine version to compare. + * @return int Returns -1 if older, 0 if it is the same, 1 if version + * passed as argument is newer. + */ + public static function compare($version) + { + $currentVersion = str_replace(' ', '', strtolower(self::VERSION)); + $version = str_replace(' ', '', $version); + + return version_compare($version, $currentVersion); + } +} diff --git a/Doctrine/Symfony/Component/Console/Application.php b/Doctrine/Symfony/Component/Console/Application.php new file mode 100644 index 0000000..7db8f1a --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Application.php @@ -0,0 +1,743 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * An Application is the container for a collection of commands. + * + * It is the main entry point of a Console application. + * + * This class is optimized for a standard CLI environment. + * + * Usage: + * + * $app = new Application('myapp', '1.0 (stable)'); + * $app->add(new SimpleCommand()); + * $app->run(); + * + * @author Fabien Potencier + */ +class Application +{ + protected $commands; + protected $aliases; + protected $wantHelps = false; + protected $runningCommand; + protected $name; + protected $version; + protected $catchExceptions; + protected $autoExit; + protected $definition; + protected $helperSet; + + /** + * Constructor. + * + * @param string $name The name of the application + * @param string $version The version of the application + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + $this->catchExceptions = true; + $this->autoExit = true; + $this->commands = array(); + $this->aliases = array(); + $this->helperSet = new HelperSet(array( + new FormatterHelper(), + new DialogHelper(), + )); + + $this->add(new HelpCommand()); + $this->add(new ListCommand()); + + $this->definition = new InputDefinition(array( + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display this help message.'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message.'), + new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Increase verbosity of messages.'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this program version.'), + new InputOption('--ansi', '-a', InputOption::VALUE_NONE, 'Force ANSI output.'), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question.'), + )); + } + + /** + * Runs the current application. + * + * @param InputInterface $input An Input instance + * @param OutputInterface $output An Output instance + * + * @return integer 0 if everything went fine, or an error code + * + * @throws \Exception When doRun returns Exception + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + if (null === $input) { + $input = new ArgvInput(); + } + + if (null === $output) { + $output = new ConsoleOutput(); + } + + try { + $statusCode = $this->doRun($input, $output); + } catch (\Exception $e) { + if (!$this->catchExceptions) { + throw $e; + } + + $this->renderException($e, $output); + $statusCode = $e->getCode(); + + $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1; + } + + if ($this->autoExit) { + if ($statusCode > 255) { + $statusCode = 255; + } + // @codeCoverageIgnoreStart + exit($statusCode); + // @codeCoverageIgnoreEnd + } else { + return $statusCode; + } + } + + /** + * Runs the current application. + * + * @param InputInterface $input An Input instance + * @param OutputInterface $output An Output instance + * + * @return integer 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + $name = $this->getCommandName($input); + + if (true === $input->hasParameterOption(array('--ansi', '-a'))) { + $output->setDecorated(true); + } + + if (true === $input->hasParameterOption(array('--help', '-h'))) { + if (!$name) { + $name = 'help'; + $input = new ArrayInput(array('command' => 'help')); + } else { + $this->wantHelps = true; + } + } + + if (true === $input->hasParameterOption(array('--no-interaction', '-n'))) { + $input->setInteractive(false); + } + + if (true === $input->hasParameterOption(array('--quiet', '-q'))) { + $output->setVerbosity(Output::VERBOSITY_QUIET); + } elseif (true === $input->hasParameterOption(array('--verbose', '-v'))) { + $output->setVerbosity(Output::VERBOSITY_VERBOSE); + } + + if (true === $input->hasParameterOption(array('--version', '-V'))) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + if (!$name) { + $name = 'list'; + $input = new ArrayInput(array('command' => 'list')); + } + + // the command name MUST be the first element of the input + $command = $this->find($name); + + $this->runningCommand = $command; + $statusCode = $command->run($input, $output); + $this->runningCommand = null; + + return is_numeric($statusCode) ? $statusCode : 0; + } + + /** + * Set a helper set to be used with the command. + * + * @param HelperSet $helperSet The helper set + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Get the helper set associated with the command + * + * @return HelperSet The HelperSet instance associated with this command + */ + public function getHelperSet() + { + return $this->helperSet; + } + + /** + * Gets the InputDefinition related to this Application. + * + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * + * @return string A help message. + */ + public function getHelp() + { + $messages = array( + $this->getLongVersion(), + '', + 'Usage:', + sprintf(" [options] command [arguments]\n"), + 'Options:', + ); + + foreach ($this->definition->getOptions() as $option) { + $messages[] = sprintf(' %-29s %s %s', + '--'.$option->getName().'', + $option->getShortcut() ? '-'.$option->getShortcut().'' : ' ', + $option->getDescription() + ); + } + + return implode("\n", $messages); + } + + /** + * Sets whether to catch exceptions or not during commands execution. + * + * @param Boolean $boolean Whether to catch exceptions or not during commands execution + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (Boolean) $boolean; + } + + /** + * Sets whether to automatically exit after a command execution or not. + * + * @param Boolean $boolean Whether to automatically exit after a command execution or not + */ + public function setAutoExit($boolean) + { + $this->autoExit = (Boolean) $boolean; + } + + /** + * Gets the name of the application. + * + * @return string The application name + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the application name. + * + * @param string $name The application name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Gets the application version. + * + * @return string The application version + */ + public function getVersion() + { + return $this->version; + } + + /** + * Sets the application version. + * + * @param string $version The application version + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * Returns the long version of the application. + * + * @return string The long application version + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } else { + return 'Console Tool'; + } + } + + /** + * Registers a new command. + * + * @param string $name The command name + * + * @return Command The newly created command + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * Adds an array of command objects. + * + * @param Command[] $commands An array of commands + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * Adds a command object. + * + * If a command with the same name already exists, it will be overridden. + * + * @param Command $command A Command object + * + * @return Command The registered command + */ + public function add(Command $command) + { + $command->setApplication($this); + + $this->commands[$command->getFullName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->aliases[$alias] = $command; + } + + return $command; + } + + /** + * Returns a registered command by name or alias. + * + * @param string $name The command name or alias + * + * @return Command A Command object + * + * @throws \InvalidArgumentException When command name given does not exist + */ + public function get($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + } + + $command = isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * Returns true if the command exists, false otherwise + * + * @param string $name The command name or alias + * + * @return Boolean true if the command exists, false otherwise + */ + public function has($name) + { + return isset($this->commands[$name]) || isset($this->aliases[$name]); + } + + /** + * Returns an array of all unique namespaces used by currently registered commands. + * + * It does not returns the global namespace which always exists. + * + * @return array An array of namespaces + */ + public function getNamespaces() + { + $namespaces = array(); + foreach ($this->commands as $command) { + if ($command->getNamespace()) { + $namespaces[$command->getNamespace()] = true; + } + } + + return array_keys($namespaces); + } + + /** + * Finds a registered namespace by a name or an abbreviation. + * + * @return string A registered namespace + * + * @throws \InvalidArgumentException When namespace is incorrect or ambiguous + */ + public function findNamespace($namespace) + { + $abbrevs = static::getAbbreviations($this->getNamespaces()); + + if (!isset($abbrevs[$namespace])) { + throw new \InvalidArgumentException(sprintf('There are no commands defined in the "%s" namespace.', $namespace)); + } + + if (count($abbrevs[$namespace]) > 1) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions($abbrevs[$namespace]))); + } + + return $abbrevs[$namespace][0]; + } + + /** + * Finds a command by name or alias. + * + * Contrary to get, this command tries to find the best + * match if you give it an abbreviation of a name or alias. + * + * @param string $name A command name or a command alias + * + * @return Command A Command instance + * + * @throws \InvalidArgumentException When command name is incorrect or ambiguous + */ + public function find($name) + { + // namespace + $namespace = ''; + if (false !== $pos = strrpos($name, ':')) { + $namespace = $this->findNamespace(substr($name, 0, $pos)); + $name = substr($name, $pos + 1); + } + + $fullName = $namespace ? $namespace.':'.$name : $name; + + // name + $commands = array(); + foreach ($this->commands as $command) { + if ($command->getNamespace() == $namespace) { + $commands[] = $command->getName(); + } + } + + $abbrevs = static::getAbbreviations($commands); + if (isset($abbrevs[$name]) && 1 == count($abbrevs[$name])) { + return $this->get($namespace ? $namespace.':'.$abbrevs[$name][0] : $abbrevs[$name][0]); + } + + if (isset($abbrevs[$name]) && count($abbrevs[$name]) > 1) { + $suggestions = $this->getAbbreviationSuggestions(array_map(function ($command) use ($namespace) { return $namespace.':'.$command; }, $abbrevs[$name])); + + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $fullName, $suggestions)); + } + + // aliases + $abbrevs = static::getAbbreviations(array_keys($this->aliases)); + if (!isset($abbrevs[$fullName])) { + throw new \InvalidArgumentException(sprintf('Command "%s" is not defined.', $fullName)); + } + + if (count($abbrevs[$fullName]) > 1) { + throw new \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $fullName, $this->getAbbreviationSuggestions($abbrevs[$fullName]))); + } + + return $this->get($abbrevs[$fullName][0]); + } + + /** + * Gets the commands (registered in the given namespace if provided). + * + * The array keys are the full names and the values the command instances. + * + * @param string $namespace A namespace name + * + * @return array An array of Command instances + */ + public function all($namespace = null) + { + if (null === $namespace) { + return $this->commands; + } + + $commands = array(); + foreach ($this->commands as $name => $command) { + if ($namespace === $command->getNamespace()) { + $commands[$name] = $command; + } + } + + return $commands; + } + + /** + * Returns an array of possible abbreviations given a set of names. + * + * @param array $names An array of names + * + * @return array An array of abbreviations + */ + static public function getAbbreviations($names) + { + $abbrevs = array(); + foreach ($names as $name) { + for ($len = strlen($name) - 1; $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + if (!isset($abbrevs[$abbrev])) { + $abbrevs[$abbrev] = array($name); + } else { + $abbrevs[$abbrev][] = $name; + } + } + } + + // Non-abbreviations always get entered, even if they aren't unique + foreach ($names as $name) { + $abbrevs[$name] = array($name); + } + + return $abbrevs; + } + + /** + * Returns a text representation of the Application. + * + * @param string $namespace An optional namespace name + * + * @return string A string representing the Application + */ + public function asText($namespace = null) + { + $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; + + $messages = array($this->getHelp(), ''); + if ($namespace) { + $messages[] = sprintf("Available commands for the \"%s\" namespace:", $namespace); + } else { + $messages[] = 'Available commands:'; + } + + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + $width += 2; + + // add commands by namespace + foreach ($this->sortCommands($commands) as $space => $commands) { + if (!$namespace && '_global' !== $space) { + $messages[] = ''.$space.''; + } + + foreach ($commands as $command) { + $aliases = $command->getAliases() ? ' ('.implode(', ', $command->getAliases()).')' : ''; + + $messages[] = sprintf(" %-${width}s %s%s", ($command->getNamespace() ? ':' : '').$command->getName(), $command->getDescription(), $aliases); + } + } + + return implode("\n", $messages); + } + + /** + * Returns an XML representation of the Application. + * + * @param string $namespace An optional namespace name + * @param Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|DOMDocument An XML string representing the Application + */ + public function asXml($namespace = null, $asDom = false) + { + $commands = $namespace ? $this->all($this->findNamespace($namespace)) : $this->commands; + + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $dom->appendChild($xml = $dom->createElement('symfony')); + + $xml->appendChild($commandsXML = $dom->createElement('commands')); + + if ($namespace) { + $commandsXML->setAttribute('namespace', $namespace); + } else { + $xml->appendChild($namespacesXML = $dom->createElement('namespaces')); + } + + // add commands by namespace + foreach ($this->sortCommands($commands) as $space => $commands) { + if (!$namespace) { + $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); + $namespaceArrayXML->setAttribute('id', $space); + } + + foreach ($commands as $command) { + if (!$namespace) { + $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); + $commandXML->appendChild($dom->createTextNode($command->getName())); + } + + $node = $command->asXml(true)->getElementsByTagName('command')->item(0); + $node = $dom->importNode($node, true); + + $commandsXML->appendChild($node); + } + } + + return $asDom ? $dom : $dom->saveXml(); + } + + /** + * Renders a catched exception. + * + * @param Exception $e An exception instance + * @param OutputInterface $output An OutputInterface instance + */ + public function renderException($e, $output) + { + $strlen = function ($string) + { + return function_exists('mb_strlen') ? mb_strlen($string) : strlen($string); + }; + + $title = sprintf(' [%s] ', get_class($e)); + $len = $strlen($title); + $lines = array(); + foreach (explode("\n", $e->getMessage()) as $line) { + $lines[] = sprintf(' %s ', $line); + $len = max($strlen($line) + 4, $len); + } + + $messages = array(str_repeat(' ', $len), $title.str_repeat(' ', $len - $strlen($title))); + + foreach ($lines as $line) { + $messages[] = $line.str_repeat(' ', $len - $strlen($line)); + } + + $messages[] = str_repeat(' ', $len); + + $output->writeln("\n"); + foreach ($messages as $message) { + $output->writeln(''.$message.''); + } + $output->writeln("\n"); + + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName()))); + $output->writeln("\n"); + } + + if (Output::VERBOSITY_VERBOSE === $output->getVerbosity()) { + $output->writeln('Exception trace:'); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, array( + 'function' => '', + 'file' => $e->getFile() != null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() != null ? $e->getLine() : 'n/a', + 'args' => array(), + )); + + for ($i = 0, $count = count($trace); $i < $count; $i++) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $output->writeln(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line)); + } + + $output->writeln("\n"); + } + } + + protected function getCommandName(InputInterface $input) + { + return $input->getFirstArgument('command'); + } + + protected function sortCommands($commands) + { + $namespacedCommands = array(); + foreach ($commands as $name => $command) { + $key = $command->getNamespace() ? $command->getNamespace() : '_global'; + + if (!isset($namespacedCommands[$key])) { + $namespacedCommands[$key] = array(); + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as $name => &$commands) { + ksort($commands); + } + + return $namespacedCommands; + } + + protected function getAbbreviationSuggestions($abbrevs) + { + return sprintf('%s, %s%s', $abbrevs[0], $abbrevs[1], count($abbrevs) > 2 ? sprintf(' and %d more', count($abbrevs) - 2) : ''); + } +} diff --git a/Doctrine/Symfony/Component/Console/Command/Command.php b/Doctrine/Symfony/Component/Console/Command/Command.php new file mode 100644 index 0000000..3777d96 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Command/Command.php @@ -0,0 +1,512 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Base class for all commands. + * + * @author Fabien Potencier + */ +class Command +{ + protected $name; + protected $namespace; + protected $aliases; + protected $definition; + protected $help; + protected $application; + protected $description; + protected $ignoreValidationErrors; + protected $applicationDefinitionMerged; + protected $code; + + /** + * Constructor. + * + * @param string $name The name of the command + * + * @throws \LogicException When the command name is empty + */ + public function __construct($name = null) + { + $this->definition = new InputDefinition(); + $this->ignoreValidationErrors = false; + $this->applicationDefinitionMerged = false; + $this->aliases = array(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException('The command name cannot be empty.'); + } + } + + /** + * Sets the application instance for this command. + * + * @param Application $application An Application instance + */ + public function setApplication(Application $application = null) + { + $this->application = $application; + } + + /** + * Configures the current command. + */ + protected function configure() + { + } + + /** + * Executes the current command. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return integer 0 if everything went fine, or an error code + * + * @throws \LogicException When this abstract class is not implemented + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * Interacts with the user. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + } + + /** + * Initializes the command just after the input has been validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + } + + /** + * Runs the command. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + public function run(InputInterface $input, OutputInterface $output) + { + // add the application arguments and options + $this->mergeApplicationDefinition(); + + // bind the input against the command specific arguments/options + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + return call_user_func($this->code, $input, $output); + } else { + return $this->execute($input, $output); + } + } + + /** + * Sets the code to execute when running this command. + * + * @param \Closure $code A \Closure + * + * @return Command The current instance + */ + public function setCode(\Closure $code) + { + $this->code = $code; + + return $this; + } + + /** + * Merges the application definition with the command definition. + */ + protected function mergeApplicationDefinition() + { + if (null === $this->application || true === $this->applicationDefinitionMerged) { + return; + } + + $this->definition->setArguments(array_merge( + $this->application->getDefinition()->getArguments(), + $this->definition->getArguments() + )); + + $this->definition->addOptions($this->application->getDefinition()->getOptions()); + + $this->applicationDefinitionMerged = true; + } + + /** + * Sets an array of argument and option instances. + * + * @param array|Definition $definition An array of argument and option instances or a definition instance + * + * @return Command The current instance + */ + public function setDefinition($definition) + { + if ($definition instanceof InputDefinition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->applicationDefinitionMerged = false; + + return $this; + } + + /** + * Gets the InputDefinition attached to this Command. + * + * @return InputDefinition An InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Adds an argument. + * + * @param string $name The argument name + * @param integer $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param string $description A description text + * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) + * + * @return Command The current instance + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); + + return $this; + } + + /** + * Adds an option. + * + * @param string $name The option name + * @param string $shortcut The shortcut (can be null) + * @param integer $mode The option mode: One of the InputOption::VALUE_* constants + * @param string $description A description text + * @param mixed $default The default value (must be null for InputOption::VALUE_REQUIRED or self::VALUE_NONE) + * + * @return Command The current instance + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * Sets the name of the command. + * + * This method can set both the namespace and the name if + * you separate them by a colon (:) + * + * $command->setName('foo:bar'); + * + * @param string $name The command name + * + * @return Command The current instance + * + * @throws \InvalidArgumentException When command name given is empty + */ + public function setName($name) + { + if (false !== $pos = strrpos($name, ':')) { + $namespace = substr($name, 0, $pos); + $name = substr($name, $pos + 1); + } else { + $namespace = $this->namespace; + } + + if (!$name) { + throw new \InvalidArgumentException('A command name cannot be empty.'); + } + + $this->namespace = $namespace; + $this->name = $name; + + return $this; + } + + /** + * Returns the command namespace. + * + * @return string The command namespace + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Returns the command name + * + * @return string The command name + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the fully qualified command name. + * + * @return string The fully qualified command name + */ + public function getFullName() + { + return $this->getNamespace() ? $this->getNamespace().':'.$this->getName() : $this->getName(); + } + + /** + * Sets the description for the command. + * + * @param string $description The description for the command + * + * @return Command The current instance + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Returns the description for the command. + * + * @return string The description for the command + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the help for the command. + * + * @param string $help The help for the command + * + * @return Command The current instance + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * Returns the help for the command. + * + * @return string The help for the command + */ + public function getHelp() + { + return $this->help; + } + + /** + * Returns the processed help for the command replacing the %command.name% and + * %command.full_name% patterns with the real values dynamically. + * + * @return string The processed help for the command + */ + public function getProcessedHelp() + { + $name = $this->namespace.':'.$this->name; + + $placeholders = array( + '%command.name%', + '%command.full_name%' + ); + $replacements = array( + $name, + $_SERVER['PHP_SELF'].' '.$name + ); + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * Sets the aliases for the command. + * + * @param array $aliases An array of aliases for the command + * + * @return Command The current instance + */ + public function setAliases($aliases) + { + $this->aliases = $aliases; + + return $this; + } + + /** + * Returns the aliases for the command. + * + * @return array An array of aliases for the command + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * Returns the synopsis for the command. + * + * @return string The synopsis + */ + public function getSynopsis() + { + return sprintf('%s %s', $this->getFullName(), $this->definition->getSynopsis()); + } + + /** + * Gets a helper instance by name. + * + * @param string $name The helper name + * + * @return mixed The helper value + * + * @throws \InvalidArgumentException if the helper is not defined + */ + protected function getHelper($name) + { + return $this->application->getHelperSet()->get($name); + } + + /** + * Gets a helper instance by name. + * + * @param string $name The helper name + * + * @return mixed The helper value + * + * @throws \InvalidArgumentException if the helper is not defined + */ + public function __get($name) + { + return $this->application->getHelperSet()->get($name); + } + + /** + * Returns a text representation of the command. + * + * @return string A string representing the command + */ + public function asText() + { + $messages = array( + 'Usage:', + ' '.$this->getSynopsis(), + '', + ); + + if ($this->getAliases()) { + $messages[] = 'Aliases: '.implode(', ', $this->getAliases()).''; + } + + $messages[] = $this->definition->asText(); + + if ($help = $this->getProcessedHelp()) { + $messages[] = 'Help:'; + $messages[] = ' '.implode("\n ", explode("\n", $help))."\n"; + } + + return implode("\n", $messages); + } + + /** + * Returns an XML representation of the command. + * + * @param Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|DOMDocument An XML string representing the command + */ + public function asXml($asDom = false) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $dom->appendChild($commandXML = $dom->createElement('command')); + $commandXML->setAttribute('id', $this->getFullName()); + $commandXML->setAttribute('namespace', $this->getNamespace() ? $this->getNamespace() : '_global'); + $commandXML->setAttribute('name', $this->getName()); + + $commandXML->appendChild($usageXML = $dom->createElement('usage')); + $usageXML->appendChild($dom->createTextNode(sprintf($this->getSynopsis(), ''))); + + $commandXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode(implode("\n ", explode("\n", $this->getDescription())))); + + $commandXML->appendChild($helpXML = $dom->createElement('help')); + $help = $this->help; + $helpXML->appendChild($dom->createTextNode(implode("\n ", explode("\n", $help)))); + + $commandXML->appendChild($aliasesXML = $dom->createElement('aliases')); + foreach ($this->getAliases() as $alias) { + $aliasesXML->appendChild($aliasXML = $dom->createElement('alias')); + $aliasXML->appendChild($dom->createTextNode($alias)); + } + + $definition = $this->definition->asXml(true); + $commandXML->appendChild($dom->importNode($definition->getElementsByTagName('arguments')->item(0), true)); + $commandXML->appendChild($dom->importNode($definition->getElementsByTagName('options')->item(0), true)); + + return $asDom ? $dom : $dom->saveXml(); + } +} diff --git a/Doctrine/Symfony/Component/Console/Command/HelpCommand.php b/Doctrine/Symfony/Component/Console/Command/HelpCommand.php new file mode 100644 index 0000000..5504832 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Command/HelpCommand.php @@ -0,0 +1,77 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * HelpCommand displays the help for a given command. + * + * @author Fabien Potencier + */ +class HelpCommand extends Command +{ + protected $command; + + /** + * @see Command + */ + protected function configure() + { + $this->ignoreValidationErrors = true; + + $this + ->setDefinition(array( + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), + new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), + )) + ->setName('help') + ->setAliases(array('?')) + ->setDescription('Displays help for a command') + ->setHelp(<<help command displays help for a given command: + + ./symfony help list + +You can also output the help as XML by using the --xml option: + + ./symfony help --xml list +EOF + ); + } + + public function setCommand(Command $command) + { + $this->command = $command; + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if (null === $this->command) { + $this->command = $this->application->get($input->getArgument('command_name')); + } + + if ($input->getOption('xml')) { + $output->writeln($this->command->asXml(), Output::OUTPUT_RAW); + } else { + $output->writeln($this->command->asText()); + } + } +} diff --git a/Doctrine/Symfony/Component/Console/Command/ListCommand.php b/Doctrine/Symfony/Component/Console/Command/ListCommand.php new file mode 100644 index 0000000..a1f77e9 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Command/ListCommand.php @@ -0,0 +1,67 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ListCommand displays the list of all available commands for the application. + * + * @author Fabien Potencier + */ +class ListCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + $this + ->setDefinition(array( + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputOption('xml', null, InputOption::VALUE_NONE, 'To output help as XML'), + )) + ->setName('list') + ->setDescription('Lists commands') + ->setHelp(<<list command lists all commands: + + ./symfony list + +You can also display the commands for a specific namespace: + + ./symfony list test + +You can also output the information as XML by using the --xml option: + + ./symfony list --xml +EOF + ); + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + if ($input->getOption('xml')) { + $output->writeln($this->application->asXml($input->getArgument('namespace')), Output::OUTPUT_RAW); + } else { + $output->writeln($this->application->asText($input->getArgument('namespace'))); + } + } +} diff --git a/Doctrine/Symfony/Component/Console/Helper/DialogHelper.php b/Doctrine/Symfony/Component/Console/Helper/DialogHelper.php new file mode 100644 index 0000000..25c2b04 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Helper/DialogHelper.php @@ -0,0 +1,110 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * The Dialog class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class DialogHelper extends Helper +{ + /** + * Asks a question to the user. + * + * @param OutputInterface $output + * @param string|array $question The question to ask + * @param string $default The default answer if none is given by the user + * + * @return string The user answer + */ + public function ask(OutputInterface $output, $question, $default = null) + { + // @codeCoverageIgnoreStart + $output->writeln($question); + + $ret = trim(fgets(STDIN)); + + return $ret ? $ret : $default; + // @codeCoverageIgnoreEnd + } + + /** + * Asks a confirmation to the user. + * + * The question will be asked until the user answer by nothing, yes, or no. + * + * @param OutputInterface $output + * @param string|array $question The question to ask + * @param Boolean $default The default answer if the user enters nothing + * + * @return Boolean true if the user has confirmed, false otherwise + */ + public function askConfirmation(OutputInterface $output, $question, $default = true) + { + // @codeCoverageIgnoreStart + $answer = 'z'; + while ($answer && !in_array(strtolower($answer[0]), array('y', 'n'))) { + $answer = $this->ask($output, $question); + } + + if (false === $default) { + return $answer && 'y' == strtolower($answer[0]); + } else { + return !$answer || 'y' == strtolower($answer[0]); + } + // @codeCoverageIgnoreEnd + } + + /** + * Asks for a value and validates the response. + * + * @param OutputInterface $output + * @param string|array $question + * @param Closure $validator + * @param integer $attempts Max number of times to ask before giving up (false by default, which means infinite) + * + * @return mixed + * + * @throws \Exception When any of the validator returns an error + */ + public function askAndValidate(OutputInterface $output, $question, \Closure $validator, $attempts = false) + { + // @codeCoverageIgnoreStart + $error = null; + while (false === $attempts || $attempts--) { + if (null !== $error) { + $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error')); + } + + $value = $this->ask($output, $question, null); + + try { + return $validator($value); + } catch (\Exception $error) { + } + } + + throw $error; + // @codeCoverageIgnoreEnd + } + + /** + * Returns the helper's canonical name + */ + public function getName() + { + return 'dialog'; + } +} diff --git a/Doctrine/Symfony/Component/Console/Helper/FormatterHelper.php b/Doctrine/Symfony/Component/Console/Helper/FormatterHelper.php new file mode 100644 index 0000000..baa2bc1 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Helper/FormatterHelper.php @@ -0,0 +1,82 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * The Formatter class provides helpers to format messages. + * + * @author Fabien Potencier + */ +class FormatterHelper extends Helper +{ + /** + * Formats a message within a section. + * + * @param string $section The section name + * @param string $message The message + * @param string $style The style to apply to the section + */ + public function formatSection($section, $message, $style = 'info') + { + return sprintf('<%s>[%s] %s', $style, $section, $style, $message); + } + + /** + * Formats a message as a block of text. + * + * @param string|array $messages The message to write in the block + * @param string $style The style to apply to the whole block + * @param Boolean $large Whether to return a large block + * + * @return string The formatter message + */ + public function formatBlock($messages, $style, $large = false) + { + if (!is_array($messages)) { + $messages = array($messages); + } + + $len = 0; + $lines = array(); + foreach ($messages as $message) { + $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); + $len = max($this->strlen($message) + ($large ? 4 : 2), $len); + } + + $messages = $large ? array(str_repeat(' ', $len)) : array(); + foreach ($lines as $line) { + $messages[] = $line.str_repeat(' ', $len - $this->strlen($line)); + } + if ($large) { + $messages[] = str_repeat(' ', $len); + } + + foreach ($messages as &$message) { + $message = sprintf('<%s>%s', $style, $message, $style); + } + + return implode("\n", $messages); + } + + protected function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string) : strlen($string); + } + + /** + * Returns the helper's canonical name + */ + public function getName() + { + return 'formatter'; + } +} diff --git a/Doctrine/Symfony/Component/Console/Helper/Helper.php b/Doctrine/Symfony/Component/Console/Helper/Helper.php new file mode 100644 index 0000000..28a8d99 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Helper/Helper.php @@ -0,0 +1,42 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Helper is the base class for all helper classes. + * + * @author Fabien Potencier + */ +abstract class Helper implements HelperInterface +{ + protected $helperSet = null; + + /** + * Sets the helper set associated with this helper. + * + * @param HelperSet $helperSet A HelperSet instance + */ + public function setHelperSet(HelperSet $helperSet = null) + { + $this->helperSet = $helperSet; + } + + /** + * Gets the helper set associated with this helper. + * + * @return HelperSet A HelperSet instance + */ + public function getHelperSet() + { + return $this->helperSet; + } +} diff --git a/Doctrine/Symfony/Component/Console/Helper/HelperInterface.php b/Doctrine/Symfony/Component/Console/Helper/HelperInterface.php new file mode 100644 index 0000000..7430e07 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Helper/HelperInterface.php @@ -0,0 +1,41 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * HelperInterface is the interface all helpers must implement. + * + * @author Fabien Potencier + */ +interface HelperInterface +{ + /** + * Sets the helper set associated with this helper. + * + * @param HelperSet $helperSet A HelperSet instance + */ + function setHelperSet(HelperSet $helperSet = null); + + /** + * Gets the helper set associated with this helper. + * + * @return HelperSet A HelperSet instance + */ + function getHelperSet(); + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + */ + function getName(); +} diff --git a/Doctrine/Symfony/Component/Console/Helper/HelperSet.php b/Doctrine/Symfony/Component/Console/Helper/HelperSet.php new file mode 100644 index 0000000..b8b412f --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Helper/HelperSet.php @@ -0,0 +1,102 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * HelperSet represents a set of helpers to be used with a command. + * + * @author Fabien Potencier + */ +class HelperSet +{ + protected $helpers; + protected $command; + + /** + * @param Helper[] $helpers An array of helper. + */ + public function __construct(array $helpers = array()) + { + $this->helpers = array(); + foreach ($helpers as $alias => $helper) { + $this->set($helper, is_int($alias) ? null : $alias); + } + } + + /** + * Sets a helper. + * + * @param HelperInterface $value The helper instance + * @param string $alias An alias + */ + public function set(HelperInterface $helper, $alias = null) + { + $this->helpers[$helper->getName()] = $helper; + if (null !== $alias) { + $this->helpers[$alias] = $helper; + } + + $helper->setHelperSet($this); + } + + /** + * Returns true if the helper if defined. + * + * @param string $name The helper name + * + * @return Boolean true if the helper is defined, false otherwise + */ + public function has($name) + { + return isset($this->helpers[$name]); + } + + /** + * Gets a helper value. + * + * @param string $name The helper name + * + * @return HelperInterface The helper instance + * + * @throws \InvalidArgumentException if the helper is not defined + */ + public function get($name) + { + if (!$this->has($name)) { + throw new \InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); + } + + return $this->helpers[$name]; + } + + /** + * Sets the command associated with this helper set. + * + * @param Command $command A Command instance + */ + public function setCommand(Command $command = null) + { + $this->command = $command; + } + + /** + * Gets the command associated with this helper set. + * + * @return Command A Command instance + */ + public function getCommand() + { + return $this->command; + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/ArgvInput.php b/Doctrine/Symfony/Component/Console/Input/ArgvInput.php new file mode 100644 index 0000000..a1ca7a2 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/ArgvInput.php @@ -0,0 +1,255 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ArgvInput represents an input coming from the CLI arguments. + * + * Usage: + * + * $input = new ArgvInput(); + * + * By default, the `$_SERVER['argv']` array is used for the input values. + * + * This can be overridden by explicitly passing the input values in the constructor: + * + * $input = new ArgvInput($_SERVER['argv']); + * + * If you pass it yourself, don't forget that the first element of the array + * is the name of the running program. + * + * When passing an argument to the constructor, be sure that it respects + * the same rules as the argv one. It's almost always better to use the + * `StringInput` when you want to provide your own input. + * + * @author Fabien Potencier + * + * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 + */ +class ArgvInput extends Input +{ + protected $tokens; + protected $parsed; + + /** + * Constructor. + * + * @param array $argv An array of parameters from the CLI (in the argv format) + * @param InputDefinition $definition A InputDefinition instance + */ + public function __construct(array $argv = null, InputDefinition $definition = null) + { + if (null === $argv) { + $argv = $_SERVER['argv']; + } + + // strip the program name + array_shift($argv); + + $this->tokens = $argv; + + parent::__construct($definition); + } + + /** + * Processes command line arguments. + */ + protected function parse() + { + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + if ('--' === substr($token, 0, 2)) { + $this->parseLongOption($token); + } elseif ('-' === $token[0]) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + } + } + + /** + * Parses a short option. + * + * @param string $token The current token. + */ + protected function parseShortOption($token) + { + $name = substr($token, 1); + + if (strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { + // an option with a value (with no space) + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * Parses a short option set. + * + * @param string $token The current token + * + * @throws \RuntimeException When option given doesn't exist + */ + protected function parseShortOptionSet($name) + { + $len = strlen($name); + for ($i = 0; $i < $len; $i++) { + if (!$this->definition->hasShortcut($name[$i])) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), true); + } + } + } + + /** + * Parses a long option. + * + * @param string $token The current token + */ + protected function parseLongOption($token) + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } else { + $this->addLongOption($name, null); + } + } + + /** + * Parses an argument. + * + * @param string $token The current token + * + * @throws \RuntimeException When too many arguments are given + */ + protected function parseArgument($token) + { + if (!$this->definition->hasArgument(count($this->arguments))) { + throw new \RuntimeException('Too many arguments.'); + } + + $this->arguments[$this->definition->getArgument(count($this->arguments))->getName()] = $token; + } + + /** + * Adds a short option value. + * + * @param string $shortcut The short option key + * @param mixed $value The value for the option + * + * @throws \RuntimeException When option given doesn't exist + */ + protected function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @param string $name The long option key + * @param mixed $value The value for the option + * + * @throws \RuntimeException When option given doesn't exist + */ + protected function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (null === $value && $option->acceptValue()) { + // if option accepts an optional or mandatory argument + // let's see if there is one provided + $next = array_shift($this->parsed); + if ('-' !== $next[0]) { + $value = $next; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + + $this->options[$name] = $value; + } + + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string The value of the first argument or null otherwise + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + } + + /** + * Returns true if the raw parameters (not parsed) contains a value. + * + * This method is to be used to introspect the input parameters + * before it has been validated. It must be used carefully. + * + * @param string|array $values The value(s) to look for in the raw parameters (can be an array) + * + * @return Boolean true if the value is contained in the raw parameters + */ + public function hasParameterOption($values) + { + if (!is_array($values)) { + $values = array($values); + } + + foreach ($this->tokens as $v) { + if (in_array($v, $values)) { + return true; + } + } + + return false; + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/ArrayInput.php b/Doctrine/Symfony/Component/Console/Input/ArrayInput.php new file mode 100644 index 0000000..865e482 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/ArrayInput.php @@ -0,0 +1,162 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ArrayInput represents an input provided as an array. + * + * Usage: + * + * $input = new ArrayInput(array('name' => 'foo', '--bar' => 'foobar')); + * + * @author Fabien Potencier + */ +class ArrayInput extends Input +{ + protected $parameters; + + /** + * Constructor. + * + * @param array $param An array of parameters + * @param InputDefinition $definition A InputDefinition instance + */ + public function __construct(array $parameters, InputDefinition $definition = null) + { + $this->parameters = $parameters; + + parent::__construct($definition); + } + + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string The value of the first argument or null otherwise + */ + public function getFirstArgument() + { + foreach ($this->parameters as $key => $value) { + if ($key && '-' === $key[0]) { + continue; + } + + return $value; + } + } + + /** + * Returns true if the raw parameters (not parsed) contains a value. + * + * This method is to be used to introspect the input parameters + * before it has been validated. It must be used carefully. + * + * @param string|array $value The values to look for in the raw parameters (can be an array) + * + * @return Boolean true if the value is contained in the raw parameters + */ + public function hasParameterOption($values) + { + if (!is_array($values)) { + $values = array($values); + } + + foreach ($this->parameters as $k => $v) { + if (!is_int($k)) { + $v = $k; + } + + if (in_array($v, $values)) { + return true; + } + } + + return false; + } + + /** + * Processes command line arguments. + */ + protected function parse() + { + foreach ($this->parameters as $key => $value) { + if ('--' === substr($key, 0, 2)) { + $this->addLongOption(substr($key, 2), $value); + } elseif ('-' === $key[0]) { + $this->addShortOption(substr($key, 1), $value); + } else { + $this->addArgument($key, $value); + } + } + } + + /** + * Adds a short option value. + * + * @param string $shortcut The short option key + * @param mixed $value The value for the option + * + * @throws \RuntimeException When option given doesn't exist + */ + protected function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @param string $name The long option key + * @param mixed $value The value for the option + * + * @throws \InvalidArgumentException When option given doesn't exist + * @throws \InvalidArgumentException When a required value is missing + */ + protected function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \InvalidArgumentException(sprintf('The "--%s" option requires a value.', $name)); + } + + $value = $option->isValueOptional() ? $option->getDefault() : true; + } + + $this->options[$name] = $value; + } + + /** + * Adds an argument value. + * + * @param string $name The argument name + * @param mixed $value The value for the argument + * + * @throws \InvalidArgumentException When argument given doesn't exist + */ + protected function addArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/Input.php b/Doctrine/Symfony/Component/Console/Input/Input.php new file mode 100644 index 0000000..9819b18 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/Input.php @@ -0,0 +1,199 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Input is the base class for all concrete Input classes. + * + * Three concrete classes are provided by default: + * + * * `ArgvInput`: The input comes from the CLI arguments (argv) + * * `StringInput`: The input is provided as a string + * * `ArrayInput`: The input is provided as an array + * + * @author Fabien Potencier + */ +abstract class Input implements InputInterface +{ + protected $definition; + protected $options; + protected $arguments; + protected $interactive = true; + + /** + * Constructor. + * + * @param InputDefinition $definition A InputDefinition instance + */ + public function __construct(InputDefinition $definition = null) + { + if (null === $definition) { + $this->definition = new InputDefinition(); + } else { + $this->bind($definition); + $this->validate(); + } + } + + /** + * Binds the current Input instance with the given arguments and options. + * + * @param InputDefinition $definition A InputDefinition instance + */ + public function bind(InputDefinition $definition) + { + $this->arguments = array(); + $this->options = array(); + $this->definition = $definition; + + $this->parse(); + } + + /** + * Processes command line arguments. + */ + abstract protected function parse(); + + /** + * @throws \RuntimeException When not enough arguments are given + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + public function isInteractive() + { + return $this->interactive; + } + + public function setInteractive($interactive) + { + $this->interactive = (Boolean) $interactive; + } + + /** + * Returns the argument values. + * + * @return array An array of argument values + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * Returns the argument value for a given argument name. + * + * @param string $name The argument name + * + * @return mixed The argument value + * + * @throws \InvalidArgumentException When argument given doesn't exist + */ + public function getArgument($name) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)->getDefault(); + } + + /** + * Sets an argument value by name. + * + * @param string $name The argument name + * @param string $value The argument value + * + * @throws \InvalidArgumentException When argument given doesn't exist + */ + public function setArgument($name, $value) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * Returns true if an InputArgument object exists by name or position. + * + * @param string|integer $name The InputArgument name or position + * + * @return Boolean true if the InputArgument object exists, false otherwise + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * Returns the options values. + * + * @return array An array of option values + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * Returns the option value for a given option name. + * + * @param string $name The option name + * + * @return mixed The option value + * + * @throws \InvalidArgumentException When option given doesn't exist + */ + public function getOption($name) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * Sets an option value by name. + * + * @param string $name The option name + * @param string $value The option value + * + * @throws \InvalidArgumentException When option given doesn't exist + */ + public function setOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + /** + * Returns true if an InputOption object exists by name. + * + * @param string $name The InputOption name + * + * @return Boolean true if the InputOption object exists, false otherwise + */ + public function hasOption($name) + { + return $this->definition->hasOption($name); + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/InputArgument.php b/Doctrine/Symfony/Component/Console/Input/InputArgument.php new file mode 100644 index 0000000..f96eecb --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/InputArgument.php @@ -0,0 +1,128 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Represents a command line argument. + * + * @author Fabien Potencier + */ +class InputArgument +{ + const REQUIRED = 1; + const OPTIONAL = 2; + const IS_ARRAY = 4; + + protected $name; + protected $mode; + protected $default; + protected $description; + + /** + * Constructor. + * + * @param string $name The argument name + * @param integer $mode The argument mode: self::REQUIRED or self::OPTIONAL + * @param string $description A description text + * @param mixed $default The default value (for self::OPTIONAL mode only) + * + * @throws \InvalidArgumentException When argument mode is not valid + */ + public function __construct($name, $mode = null, $description = '', $default = null) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } else if (is_string($mode) || $mode > 7) { + throw new \InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + + $this->setDefault($default); + } + + /** + * Returns the argument name. + * + * @return string The argument name + */ + public function getName() + { + return $this->name; + } + + /** + * Returns true if the argument is required. + * + * @return Boolean true if parameter mode is self::REQUIRED, false otherwise + */ + public function isRequired() + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * Returns true if the argument can take multiple values. + * + * @return Boolean true if mode is self::IS_ARRAY, false otherwise + */ + public function isArray() + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * Sets the default value. + * + * @param mixed $default The default value + * + * @throws \LogicException When incorrect default value is given + */ + public function setDefault($default = null) + { + if (self::REQUIRED === $this->mode && null !== $default) { + throw new \LogicException('Cannot set a default value except for Parameter::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = array(); + } else if (!is_array($default)) { + throw new \LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * Returns the default value. + * + * @return mixed The default value + */ + public function getDefault() + { + return $this->default; + } + + /** + * Returns the description text. + * + * @return string The description text + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/InputDefinition.php b/Doctrine/Symfony/Component/Console/Input/InputDefinition.php new file mode 100644 index 0000000..bc2e8bc --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/InputDefinition.php @@ -0,0 +1,471 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * A InputDefinition represents a set of valid command line arguments and options. + * + * Usage: + * + * $definition = new InputDefinition(array( + * new InputArgument('name', InputArgument::REQUIRED), + * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), + * )); + * + * @author Fabien Potencier + */ +class InputDefinition +{ + protected $arguments; + protected $requiredCount; + protected $hasAnArrayArgument = false; + protected $hasOptional; + protected $options; + protected $shortcuts; + + /** + * Constructor. + * + * @param array $definition An array of InputArgument and InputOption instance + */ + public function __construct(array $definition = array()) + { + $this->setDefinition($definition); + } + + public function setDefinition(array $definition) + { + $arguments = array(); + $options = array(); + foreach ($definition as $item) { + if ($item instanceof InputOption) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * Sets the InputArgument objects. + * + * @param array $arguments An array of InputArgument objects + */ + public function setArguments($arguments = array()) + { + $this->arguments = array(); + $this->requiredCount = 0; + $this->hasOptional = false; + $this->hasAnArrayArgument = false; + $this->addArguments($arguments); + } + + /** + * Add an array of InputArgument objects. + * + * @param InputArgument[] $arguments An array of InputArgument objects + */ + public function addArguments($arguments = array()) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * Add an InputArgument object. + * + * @param InputArgument $argument An InputArgument object + * + * @throws \LogicException When incorrect argument is given + */ + public function addArgument(InputArgument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new \LogicException(sprintf('An argument with name "%s" already exist.', $argument->getName())); + } + + if ($this->hasAnArrayArgument) { + throw new \LogicException('Cannot add an argument after an array argument.'); + } + + if ($argument->isRequired() && $this->hasOptional) { + throw new \LogicException('Cannot add a required argument after an optional one.'); + } + + if ($argument->isArray()) { + $this->hasAnArrayArgument = true; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->hasOptional = true; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * Returns an InputArgument by name or by position. + * + * @param string|integer $name The InputArgument name or position + * + * @return InputArgument An InputArgument object + * + * @throws \InvalidArgumentException When argument given doesn't exist + */ + public function getArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return $arguments[$name]; + } + + /** + * Returns true if an InputArgument object exists by name or position. + * + * @param string|integer $name The InputArgument name or position + * + * @return Boolean true if the InputArgument object exists, false otherwise + */ + public function hasArgument($name) + { + $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * Gets the array of InputArgument objects. + * + * @return array An array of InputArgument objects + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Returns the number of InputArguments. + * + * @return integer The number of InputArguments + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * Returns the number of required InputArguments. + * + * @return integer The number of required InputArguments + */ + public function getArgumentRequiredCount() + { + return $this->requiredCount; + } + + /** + * Gets the default values. + * + * @return array An array of default values + */ + public function getArgumentDefaults() + { + $values = array(); + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * Sets the InputOption objects. + * + * @param array $options An array of InputOption objects + */ + public function setOptions($options = array()) + { + $this->options = array(); + $this->shortcuts = array(); + $this->addOptions($options); + } + + /** + * Add an array of InputOption objects. + * + * @param InputOption[] $options An array of InputOption objects + */ + public function addOptions($options = array()) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * Add an InputOption object. + * + * @param InputOption $option An InputOption object + * + * @throws \LogicException When option given already exist + */ + public function addOption(InputOption $option) + { + if (isset($this->options[$option->getName()])) { + throw new \LogicException(sprintf('An option named "%s" already exist.', $option->getName())); + } else if (isset($this->shortcuts[$option->getShortcut()])) { + throw new \LogicException(sprintf('An option with shortcut "%s" already exist.', $option->getShortcut())); + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + $this->shortcuts[$option->getShortcut()] = $option->getName(); + } + } + + /** + * Returns an InputOption by name. + * + * @param string $name The InputOption name + * + * @return InputOption A InputOption object + */ + public function getOption($name) + { + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * Returns true if an InputOption object exists by name. + * + * @param string $name The InputOption name + * + * @return Boolean true if the InputOption object exists, false otherwise + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * Gets the array of InputOption objects. + * + * @return array An array of InputOption objects + */ + public function getOptions() + { + return $this->options; + } + + /** + * Returns true if an InputOption object exists by shortcut. + * + * @param string $name The InputOption shortcut + * + * @return Boolean true if the InputOption object exists, false otherwise + */ + public function hasShortcut($name) + { + return isset($this->shortcuts[$name]); + } + + /** + * Gets an InputOption by shortcut. + * + * @return InputOption An InputOption object + */ + public function getOptionForShortcut($shortcut) + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * Gets an array of default values. + * + * @return array An array of all default values + */ + public function getOptionDefaults() + { + $values = array(); + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * Returns the InputOption name given a shortcut. + * + * @param string $shortcut The shortcut + * + * @return string The InputOption name + * + * @throws \InvalidArgumentException When option given does not exist + */ + protected function shortcutToName($shortcut) + { + if (!isset($this->shortcuts[$shortcut])) { + throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * Gets the synopsis. + * + * @return string The synopsis + */ + public function getSynopsis() + { + $elements = array(); + foreach ($this->getOptions() as $option) { + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('['.($option->isValueRequired() ? '%s--%s="..."' : ($option->isValueOptional() ? '%s--%s[="..."]' : '%s--%s')).']', $shortcut, $option->getName()); + } + + foreach ($this->getArguments() as $argument) { + $elements[] = sprintf($argument->isRequired() ? '%s' : '[%s]', $argument->getName().($argument->isArray() ? '1' : '')); + + if ($argument->isArray()) { + $elements[] = sprintf('... [%sN]', $argument->getName()); + } + } + + return implode(' ', $elements); + } + + /** + * Returns a textual representation of the InputDefinition. + * + * @return string A string representing the InputDefinition + */ + public function asText() + { + // find the largest option or argument name + $max = 0; + foreach ($this->getOptions() as $option) { + $max = strlen($option->getName()) + 2 > $max ? strlen($option->getName()) + 2 : $max; + } + foreach ($this->getArguments() as $argument) { + $max = strlen($argument->getName()) > $max ? strlen($argument->getName()) : $max; + } + ++$max; + + $text = array(); + + if ($this->getArguments()) { + $text[] = 'Arguments:'; + foreach ($this->getArguments() as $argument) { + if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) { + $default = sprintf(' (default: %s)', is_array($argument->getDefault()) ? str_replace("\n", '', var_export($argument->getDefault(), true)): $argument->getDefault()); + } else { + $default = ''; + } + + $text[] = sprintf(" %-${max}s %s%s", $argument->getName(), $argument->getDescription(), $default); + } + + $text[] = ''; + } + + if ($this->getOptions()) { + $text[] = 'Options:'; + + foreach ($this->getOptions() as $option) { + if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) { + $default = sprintf(' (default: %s)', is_array($option->getDefault()) ? str_replace("\n", '', print_r($option->getDefault(), true)): $option->getDefault()); + } else { + $default = ''; + } + + $multiple = $option->isArray() ? ' (multiple values allowed)' : ''; + $text[] = sprintf(' %-'.$max.'s %s%s%s%s', '--'.$option->getName().'', $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', $option->getDescription(), $default, $multiple); + } + + $text[] = ''; + } + + return implode("\n", $text); + } + + /** + * Returns an XML representation of the InputDefinition. + * + * @param Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|DOMDocument An XML string representing the InputDefinition + */ + public function asXml($asDom = false) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $dom->appendChild($definitionXML = $dom->createElement('definition')); + + $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); + foreach ($this->getArguments() as $argument) { + $argumentsXML->appendChild($argumentXML = $dom->createElement('argument')); + $argumentXML->setAttribute('name', $argument->getName()); + $argumentXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); + $argumentXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); + $argumentXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); + + $argumentXML->appendChild($defaultsXML = $dom->createElement('defaults')); + $defaults = is_array($argument->getDefault()) ? $argument->getDefault() : ($argument->getDefault() ? array($argument->getDefault()) : array()); + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + } + + $definitionXML->appendChild($optionsXML = $dom->createElement('options')); + foreach ($this->getOptions() as $option) { + $optionsXML->appendChild($optionXML = $dom->createElement('option')); + $optionXML->setAttribute('name', '--'.$option->getName()); + $optionXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); + $optionXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); + $optionXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); + $optionXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); + $optionXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); + + if ($option->acceptValue()) { + $optionXML->appendChild($defaultsXML = $dom->createElement('defaults')); + $defaults = is_array($option->getDefault()) ? $option->getDefault() : ($option->getDefault() ? array($option->getDefault()) : array()); + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + } + } + + return $asDom ? $dom : $dom->saveXml(); + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/InputInterface.php b/Doctrine/Symfony/Component/Console/Input/InputInterface.php new file mode 100644 index 0000000..a0f1aa0 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/InputInterface.php @@ -0,0 +1,56 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * InputInterface is the interface implemented by all input classes. + * + * @author Fabien Potencier + */ +interface InputInterface +{ + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string The value of the first argument or null otherwise + */ + function getFirstArgument(); + + /** + * Returns true if the raw parameters (not parsed) contains a value. + * + * This method is to be used to introspect the input parameters + * before it has been validated. It must be used carefully. + * + * @param string $value The value to look for in the raw parameters + * + * @return Boolean true if the value is contained in the raw parameters + */ + function hasParameterOption($value); + + /** + * Binds the current Input instance with the given arguments and options. + * + * @param InputDefinition $definition A InputDefinition instance + */ + function bind(InputDefinition $definition); + + function validate(); + + function getArguments(); + + function getArgument($name); + + function getOptions(); + + function getOption($name); +} diff --git a/Doctrine/Symfony/Component/Console/Input/InputOption.php b/Doctrine/Symfony/Component/Console/Input/InputOption.php new file mode 100644 index 0000000..3b22bbe --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/InputOption.php @@ -0,0 +1,178 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Represents a command line option. + * + * @author Fabien Potencier + */ +class InputOption +{ + const VALUE_NONE = 1; + const VALUE_REQUIRED = 2; + const VALUE_OPTIONAL = 4; + const VALUE_IS_ARRAY = 8; + + protected $name; + protected $shortcut; + protected $mode; + protected $default; + protected $description; + + /** + * Constructor. + * + * @param string $name The option name + * @param string $shortcut The shortcut (can be null) + * @param integer $mode The option mode: One of the VALUE_* constants + * @param string $description A description text + * @param mixed $default The default value (must be null for self::VALUE_REQUIRED or self::VALUE_NONE) + * + * @throws \InvalidArgumentException If option mode is invalid or incompatible + */ + public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + if ('--' === substr($name, 0, 2)) { + $name = substr($name, 2); + } + + if (empty($shortcut)) { + $shortcut = null; + } + + if (null !== $shortcut) { + if ('-' === $shortcut[0]) { + $shortcut = substr($shortcut, 1); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } else if (!is_int($mode) || $mode > 15) { + throw new \InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + + if ($this->isArray() && !$this->acceptValue()) { + throw new \InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + + $this->setDefault($default); + } + + /** + * Returns the shortcut. + * + * @return string The shortcut + */ + public function getShortcut() + { + return $this->shortcut; + } + + /** + * Returns the name. + * + * @return string The name + */ + public function getName() + { + return $this->name; + } + + /** + * Returns true if the option accepts a value. + * + * @return Boolean true if value mode is not self::VALUE_NONE, false otherwise + */ + public function acceptValue() + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * Returns true if the option requires a value. + * + * @return Boolean true if value mode is self::VALUE_REQUIRED, false otherwise + */ + public function isValueRequired() + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * Returns true if the option takes an optional value. + * + * @return Boolean true if value mode is self::VALUE_OPTIONAL, false otherwise + */ + public function isValueOptional() + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * Returns true if the option can take multiple values. + * + * @return Boolean true if mode is self::VALUE_IS_ARRAY, false otherwise + */ + public function isArray() + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + /** + * Sets the default value. + * + * @param mixed $default The default value + */ + public function setDefault($default = null) + { + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new \LogicException('Cannot set a default value when using Option::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = array(); + } elseif (!is_array($default)) { + throw new \LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() ? $default : false; + } + + /** + * Returns the default value. + * + * @return mixed The default value + */ + public function getDefault() + { + return $this->default; + } + + /** + * Returns the description text. + * + * @return string The description text + */ + public function getDescription() + { + return $this->description; + } +} diff --git a/Doctrine/Symfony/Component/Console/Input/StringInput.php b/Doctrine/Symfony/Component/Console/Input/StringInput.php new file mode 100644 index 0000000..bc8cc2c --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Input/StringInput.php @@ -0,0 +1,71 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * StringInput represents an input provided as a string. + * + * Usage: + * + * $input = new StringInput('foo --bar="foobar"'); + * + * @author Fabien Potencier + */ +class StringInput extends ArgvInput +{ + const REGEX_STRING = '([^ ]+?)(?: |(?tokens = $this->tokenize($input); + } + + /** + * @throws \InvalidArgumentException When unable to parse input (should never happen) + */ + protected function tokenize($input) + { + $input = preg_replace('/(\r\n|\r|\n|\t)/', ' ', $input); + + $tokens = array(); + $length = strlen($input); + $cursor = 0; + while ($cursor < $length) { + if (preg_match('/\s+/A', $input, $match, null, $cursor)) { + } elseif (preg_match('/([^="\' ]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) { + $tokens[] = $match[1].$match[2].stripcslashes(str_replace(array('"\'', '\'"', '\'\'', '""'), '', substr($match[3], 1, strlen($match[3]) - 2))); + } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) { + $tokens[] = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)); + } elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) { + $tokens[] = stripcslashes($match[1]); + } else { + // should never happen + // @codeCoverageIgnoreStart + throw new \InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); + // @codeCoverageIgnoreEnd + } + + $cursor += strlen($match[0]); + } + + return $tokens; + } +} diff --git a/Doctrine/Symfony/Component/Console/Output/ConsoleOutput.php b/Doctrine/Symfony/Component/Console/Output/ConsoleOutput.php new file mode 100644 index 0000000..9aa4791 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Output/ConsoleOutput.php @@ -0,0 +1,39 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * ConsoleOutput is the default class for all CLI output. It uses STDOUT. + * + * This class is a convenient wrapper around `StreamOutput`. + * + * $output = new ConsoleOutput(); + * + * This is equivalent to: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * + * @author Fabien Potencier + */ +class ConsoleOutput extends StreamOutput +{ + /** + * Constructor. + * + * @param integer $verbosity The verbosity level (self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE) + * @param Boolean $decorated Whether to decorate messages or not (null for auto-guessing) + */ + public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null) + { + parent::__construct(fopen('php://stdout', 'w'), $verbosity, $decorated); + } +} diff --git a/Doctrine/Symfony/Component/Console/Output/NullOutput.php b/Doctrine/Symfony/Component/Console/Output/NullOutput.php new file mode 100644 index 0000000..695ca0e --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Output/NullOutput.php @@ -0,0 +1,32 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * NullOutput suppresses all output. + * + * $output = new NullOutput(); + * + * @author Fabien Potencier + */ +class NullOutput extends Output +{ + /** + * Writes a message to the output. + * + * @param string $message A message to write to the output + * @param Boolean $newline Whether to add a newline or not + */ + public function doWrite($message, $newline) + { + } +} diff --git a/Doctrine/Symfony/Component/Console/Output/Output.php b/Doctrine/Symfony/Component/Console/Output/Output.php new file mode 100644 index 0000000..f4542f4 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Output/Output.php @@ -0,0 +1,231 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * Base class for output classes. + * + * There is three level of verbosity: + * + * * normal: no option passed (normal output - information) + * * verbose: -v (more output - debug) + * * quiet: -q (no output) + * + * @author Fabien Potencier + */ +abstract class Output implements OutputInterface +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + protected $verbosity; + protected $decorated; + + static protected $styles = array( + 'error' => array('bg' => 'red', 'fg' => 'white'), + 'info' => array('fg' => 'green'), + 'comment' => array('fg' => 'yellow'), + 'question' => array('bg' => 'cyan', 'fg' => 'black'), + ); + static protected $options = array('bold' => 1, 'underscore' => 4, 'blink' => 5, 'reverse' => 7, 'conceal' => 8); + static protected $foreground = array('black' => 30, 'red' => 31, 'green' => 32, 'yellow' => 33, 'blue' => 34, 'magenta' => 35, 'cyan' => 36, 'white' => 37); + static protected $background = array('black' => 40, 'red' => 41, 'green' => 42, 'yellow' => 43, 'blue' => 44, 'magenta' => 45, 'cyan' => 46, 'white' => 47); + + /** + * Constructor. + * + * @param integer $verbosity The verbosity level (self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE) + * @param Boolean $decorated Whether to decorate messages or not (null for auto-guessing) + */ + public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null) + { + $this->decorated = (Boolean) $decorated; + $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity; + } + + /** + * Sets a new style. + * + * @param string $name The style name + * @param array $options An array of options + */ + static public function setStyle($name, $options = array()) + { + static::$styles[strtolower($name)] = $options; + } + + /** + * Sets the decorated flag. + * + * @param Boolean $decorated Whether to decorated the messages or not + */ + public function setDecorated($decorated) + { + $this->decorated = (Boolean) $decorated; + } + + /** + * Gets the decorated flag. + * + * @return Boolean true if the output will decorate messages, false otherwise + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * Sets the verbosity of the output. + * + * @param integer $level The level of verbosity + */ + public function setVerbosity($level) + { + $this->verbosity = (int) $level; + } + + /** + * Gets the current verbosity of the output. + * + * @return integer The current level of verbosity + */ + public function getVerbosity() + { + return $this->verbosity; + } + + /** + * Writes a message to the output and adds a newline at the end. + * + * @param string|array $messages The message as an array of lines of a single string + * @param integer $type The type of output + */ + public function writeln($messages, $type = 0) + { + $this->write($messages, true, $type); + } + + /** + * Writes a message to the output. + * + * @param string|array $messages The message as an array of lines of a single string + * @param Boolean $newline Whether to add a newline or not + * @param integer $type The type of output + * + * @throws \InvalidArgumentException When unknown output type is given + */ + public function write($messages, $newline = false, $type = 0) + { + if (self::VERBOSITY_QUIET === $this->verbosity) { + return; + } + + if (!is_array($messages)) { + $messages = array($messages); + } + + foreach ($messages as $message) { + switch ($type) { + case Output::OUTPUT_NORMAL: + $message = $this->format($message); + break; + case Output::OUTPUT_RAW: + break; + case Output::OUTPUT_PLAIN: + $message = strip_tags($this->format($message)); + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline); + } + } + + /** + * Writes a message to the output. + * + * @param string $message A message to write to the output + * @param Boolean $newline Whether to add a newline or not + */ + abstract public function doWrite($message, $newline); + + /** + * Formats a message according to the given styles. + * + * @param string $message The message to style + * + * @return string The styled message + */ + protected function format($message) + { + $message = preg_replace_callback('#<([a-z][a-z0-9\-_=;]+)>#i', array($this, 'replaceStartStyle'), $message); + + return preg_replace_callback('##i', array($this, 'replaceEndStyle'), $message); + } + + /** + * @throws \InvalidArgumentException When style is unknown + */ + protected function replaceStartStyle($match) + { + if (!$this->decorated) { + return ''; + } + + if (isset(static::$styles[strtolower($match[1])])) { + $parameters = static::$styles[strtolower($match[1])]; + } else { + // bg=blue;fg=red + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', strtolower($match[1]), $matches, PREG_SET_ORDER)) { + throw new \InvalidArgumentException(sprintf('Unknown style "%s".', $match[1])); + } + + $parameters = array(); + foreach ($matches as $match) { + $parameters[$match[1]] = $match[2]; + } + } + + $codes = array(); + + if (isset($parameters['fg'])) { + $codes[] = static::$foreground[$parameters['fg']]; + } + + if (isset($parameters['bg'])) { + $codes[] = static::$background[$parameters['bg']]; + } + + foreach (static::$options as $option => $value) { + if (isset($parameters[$option]) && $parameters[$option]) { + $codes[] = $value; + } + } + + return "\033[".implode(';', $codes).'m'; + } + + protected function replaceEndStyle($match) + { + if (!$this->decorated) { + return ''; + } + + return "\033[0m"; + } +} diff --git a/Doctrine/Symfony/Component/Console/Output/OutputInterface.php b/Doctrine/Symfony/Component/Console/Output/OutputInterface.php new file mode 100644 index 0000000..289f4b4 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Output/OutputInterface.php @@ -0,0 +1,45 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * OutputInterface is the interface implemented by all Output classes. + * + * @author Fabien Potencier + */ +interface OutputInterface +{ + /** + * Writes a message to the output. + * + * @param string|array $messages The message as an array of lines of a single string + * @param Boolean $newline Whether to add a newline or not + * @param integer $type The type of output + * + * @throws \InvalidArgumentException When unknown output type is given + */ + function write($messages, $newline = false, $type = 0); + + /** + * Sets the verbosity of the output. + * + * @param integer $level The level of verbosity + */ + function setVerbosity($level); + + /** + * Sets the decorated flag. + * + * @param Boolean $decorated Whether to decorated the messages or not + */ + function setDecorated($decorated); +} diff --git a/Doctrine/Symfony/Component/Console/Output/StreamOutput.php b/Doctrine/Symfony/Component/Console/Output/StreamOutput.php new file mode 100644 index 0000000..55805c7 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Output/StreamOutput.php @@ -0,0 +1,105 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * StreamOutput writes the output to a given stream. + * + * Usage: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * + * As `StreamOutput` can use any stream, you can also use a file: + * + * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); + * + * @author Fabien Potencier + */ +class StreamOutput extends Output +{ + protected $stream; + + /** + * Constructor. + * + * @param mixed $stream A stream resource + * @param integer $verbosity The verbosity level (self::VERBOSITY_QUIET, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE) + * @param Boolean $decorated Whether to decorate messages or not (null for auto-guessing) + * + * @throws \InvalidArgumentException When first argument is not a real stream + */ + public function __construct($stream, $verbosity = self::VERBOSITY_NORMAL, $decorated = null) + { + if (!is_resource($stream) || 'stream' !== get_resource_type($stream)) { + throw new \InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); + } + + $this->stream = $stream; + + if (null === $decorated) { + $decorated = $this->hasColorSupport($decorated); + } + + parent::__construct($verbosity, $decorated); + } + + /** + * Gets the stream attached to this StreamOutput instance. + * + * @return resource A stream resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * Writes a message to the output. + * + * @param string $message A message to write to the output + * @param Boolean $newline Whether to add a newline or not + * + * @throws \RuntimeException When unable to write output (should never happen) + */ + public function doWrite($message, $newline) + { + if (false === @fwrite($this->stream, $message.($newline ? PHP_EOL : ''))) { + // @codeCoverageIgnoreStart + // should never happen + throw new \RuntimeException('Unable to write output.'); + // @codeCoverageIgnoreEnd + } + + flush(); + } + + /** + * Returns true if the stream supports colorization. + * + * Colorization is disabled if not supported by the stream: + * + * - windows without ansicon + * - non tty consoles + * + * @return Boolean true if the stream supports colorization, false otherwise + */ + protected function hasColorSupport() + { + // @codeCoverageIgnoreStart + if (DIRECTORY_SEPARATOR == '\\') { + return false !== getenv('ANSICON'); + } else { + return function_exists('posix_isatty') && @posix_isatty($this->stream); + } + // @codeCoverageIgnoreEnd + } +} diff --git a/Doctrine/Symfony/Component/Console/Shell.php b/Doctrine/Symfony/Component/Console/Shell.php new file mode 100644 index 0000000..38b2fbb --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Shell.php @@ -0,0 +1,136 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * A Shell wraps an Application to add shell capabilities to it. + * + * This class only works with a PHP compiled with readline support + * (either --with-readline or --with-libedit) + * + * @author Fabien Potencier + */ +class Shell +{ + protected $application; + protected $history; + protected $output; + + /** + * Constructor. + * + * If there is no readline support for the current PHP executable + * a \RuntimeException exception is thrown. + * + * @param Application $application An application instance + * + * @throws \RuntimeException When Readline extension is not enabled + */ + public function __construct(Application $application) + { + if (!function_exists('readline')) { + throw new \RuntimeException('Unable to start the shell as the Readline extension is not enabled.'); + } + + $this->application = $application; + $this->history = getenv('HOME').'/.history_'.$application->getName(); + $this->output = new ConsoleOutput(); + } + + /** + * Runs the shell. + */ + public function run() + { + $this->application->setAutoExit(false); + $this->application->setCatchExceptions(true); + + readline_read_history($this->history); + readline_completion_function(array($this, 'autocompleter')); + + $this->output->writeln($this->getHeader()); + while (true) { + $command = readline($this->application->getName().' > '); + + if (false === $command) { + $this->output->writeln("\n"); + + break; + } + + readline_add_history($command); + readline_write_history($this->history); + + if (0 !== $ret = $this->application->run(new StringInput($command), $this->output)) { + $this->output->writeln(sprintf('The command terminated with an error status (%s)', $ret)); + } + } + } + + /** + * Tries to return autocompletion for the current entered text. + * + * @param string $text The last segment of the entered text + * @param integer $position The current position + */ + protected function autocompleter($text, $position) + { + $info = readline_info(); + $text = substr($info['line_buffer'], 0, $info['end']); + + if ($info['point'] !== $info['end']) { + return true; + } + + // task name? + if (false === strpos($text, ' ') || !$text) { + return array_keys($this->application->all()); + } + + // options and arguments? + try { + $command = $this->application->findCommand(substr($text, 0, strpos($text, ' '))); + } catch (\Exception $e) { + return true; + } + + $list = array('--help'); + foreach ($command->getDefinition()->getOptions() as $option) { + $list[] = '--'.$option->getName(); + } + + return $list; + } + + /** + * Returns the shell header. + * + * @return string The header string + */ + protected function getHeader() + { + return <<{$this->application->getName()} shell ({$this->application->getVersion()}). + +At the prompt, type help for some help, +or list to get a list available commands. + +To exit the shell, type ^D. + +EOF; + } +} diff --git a/Doctrine/Symfony/Component/Console/Tester/ApplicationTester.php b/Doctrine/Symfony/Component/Console/Tester/ApplicationTester.php new file mode 100644 index 0000000..b709247 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Tester/ApplicationTester.php @@ -0,0 +1,101 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * @author Fabien Potencier + */ +class ApplicationTester +{ + protected $application; + protected $display; + protected $input; + protected $output; + + /** + * Constructor. + * + * @param Application $application A Application instance to test. + */ + public function __construct(Application $application) + { + $this->application = $application; + } + + /** + * Executes the application. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * + * @param array $input An array of arguments and options + * @param array $options An array of options + */ + public function run(array $input, $options = array()) + { + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + + $ret = $this->application->run($this->input, $this->output); + + rewind($this->output->getStream()); + + return $this->display = stream_get_contents($this->output->getStream()); + } + + /** + * Gets the display returned by the last execution of the application. + * + * @return string The display + */ + public function getDisplay() + { + return $this->display; + } + + /** + * Gets the input instance used by the last execution of the application. + * + * @return InputInterface The current input instance + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the application. + * + * @return OutputInterface The current output instance + */ + public function getOutput() + { + return $this->output; + } +} diff --git a/Doctrine/Symfony/Component/Console/Tester/CommandTester.php b/Doctrine/Symfony/Component/Console/Tester/CommandTester.php new file mode 100644 index 0000000..8c971c0 --- /dev/null +++ b/Doctrine/Symfony/Component/Console/Tester/CommandTester.php @@ -0,0 +1,101 @@ + + * + * This source file is subject to the MIT license that is bundled + * with this source code in the file LICENSE. + */ + +/** + * @author Fabien Potencier + */ +class CommandTester +{ + protected $command; + protected $display; + protected $input; + protected $output; + + /** + * Constructor. + * + * @param Command $command A Command instance to test. + */ + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Executes the command. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * + * @param array $input An array of arguments and options + * @param array $options An array of options + */ + public function execute(array $input, array $options = array()) + { + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + + $ret = $this->command->run($this->input, $this->output); + + rewind($this->output->getStream()); + + return $this->display = stream_get_contents($this->output->getStream()); + } + + /** + * Gets the display returned by the last execution of the command. + * + * @return string The display + */ + public function getDisplay() + { + return $this->display; + } + + /** + * Gets the input instance used by the last execution of the command. + * + * @return InputInterface The current input instance + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the command. + * + * @return OutputInterface The current output instance + */ + public function getOutput() + { + return $this->output; + } +} diff --git a/Doctrine/Symfony/Component/Yaml/Dumper.php b/Doctrine/Symfony/Component/Yaml/Dumper.php new file mode 100644 index 0000000..4a29d8b --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/Dumper.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Dumper dumps PHP variables to YAML strings. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class Dumper +{ + /** + * Dumps a PHP value to YAML. + * + * @param mixed $input The PHP value + * @param integer $inline The level where you switch to inline YAML + * @param integer $indent The level o indentation indentation (used internally) + * + * @return string The YAML representation of the PHP value + */ + public function dump($input, $inline = 0, $indent = 0) + { + $output = ''; + $prefix = $indent ? str_repeat(' ', $indent) : ''; + + if ($inline <= 0 || !is_array($input) || empty($input)) + { + $output .= $prefix.Inline::dump($input); + } + else + { + $isAHash = array_keys($input) !== range(0, count($input) - 1); + + foreach ($input as $key => $value) + { + $willBeInlined = $inline - 1 <= 0 || !is_array($value) || empty($value); + + $output .= sprintf('%s%s%s%s', + $prefix, + $isAHash ? Inline::dump($key).':' : '-', + $willBeInlined ? ' ' : "\n", + $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + 2) + ).($willBeInlined ? "\n" : ''); + } + } + + return $output; + } +} diff --git a/Doctrine/Symfony/Component/Yaml/Exception.php b/Doctrine/Symfony/Component/Yaml/Exception.php new file mode 100644 index 0000000..c116c91 --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/Exception.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception class used by all exceptions thrown by the component. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class Exception extends \Exception +{ +} diff --git a/Doctrine/Symfony/Component/Yaml/Inline.php b/Doctrine/Symfony/Component/Yaml/Inline.php new file mode 100644 index 0000000..9159d00 --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/Inline.php @@ -0,0 +1,410 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Inline implements a YAML parser/dumper for the YAML inline syntax. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class Inline +{ + const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')'; + + /** + * Convert a YAML string to a PHP array. + * + * @param string $value A YAML string + * + * @return array A PHP array representing the YAML string + */ + static public function load($value) + { + $value = trim($value); + + if (0 == strlen($value)) + { + return ''; + } + + switch ($value[0]) + { + case '[': + return self::parseSequence($value); + case '{': + return self::parseMapping($value); + default: + return self::parseScalar($value); + } + } + + /** + * Dumps a given PHP variable to a YAML string. + * + * @param mixed $value The PHP variable to convert + * + * @return string The YAML string representing the PHP array + */ + static public function dump($value) + { + $trueValues = '1.1' == Yaml::getSpecVersion() ? array('true', 'on', '+', 'yes', 'y') : array('true'); + $falseValues = '1.1' == Yaml::getSpecVersion() ? array('false', 'off', '-', 'no', 'n') : array('false'); + + switch (true) + { + case is_resource($value): + throw new Exception('Unable to dump PHP resources in a YAML file.'); + case is_object($value): + return '!!php/object:'.serialize($value); + case is_array($value): + return self::dumpArray($value); + case null === $value: + return 'null'; + case true === $value: + return 'true'; + case false === $value: + return 'false'; + case ctype_digit($value): + return is_string($value) ? "'$value'" : (int) $value; + case is_numeric($value): + return is_infinite($value) ? str_ireplace('INF', '.Inf', strval($value)) : (is_string($value) ? "'$value'" : $value); + case false !== strpos($value, "\n") || false !== strpos($value, "\r"): + return sprintf('"%s"', str_replace(array('"', "\n", "\r"), array('\\"', '\n', '\r'), $value)); + case preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ - ? | < > = ! % @ ` ]/x', $value): + return sprintf("'%s'", str_replace('\'', '\'\'', $value)); + case '' == $value: + return "''"; + case preg_match(self::getTimestampRegex(), $value): + return "'$value'"; + case in_array(strtolower($value), $trueValues): + return "'$value'"; + case in_array(strtolower($value), $falseValues): + return "'$value'"; + case in_array(strtolower($value), array('null', '~')): + return "'$value'"; + default: + return $value; + } + } + + /** + * Dumps a PHP array to a YAML string. + * + * @param array $value The PHP array to dump + * + * @return string The YAML string representing the PHP array + */ + static protected function dumpArray($value) + { + // array + $keys = array_keys($value); + if ( + (1 == count($keys) && '0' == $keys[0]) + || + (count($keys) > 1 && array_reduce($keys, function ($v, $w) { return (integer) $v + $w; }, 0) == count($keys) * (count($keys) - 1) / 2)) + { + $output = array(); + foreach ($value as $val) + { + $output[] = self::dump($val); + } + + return sprintf('[%s]', implode(', ', $output)); + } + + // mapping + $output = array(); + foreach ($value as $key => $val) + { + $output[] = sprintf('%s: %s', self::dump($key), self::dump($val)); + } + + return sprintf('{ %s }', implode(', ', $output)); + } + + /** + * Parses a scalar to a YAML string. + * + * @param scalar $scalar + * @param string $delimiters + * @param array $stringDelimiter + * @param integer $i + * @param boolean $evaluate + * + * @return string A YAML string + */ + static public function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true) + { + if (in_array($scalar[$i], $stringDelimiters)) + { + // quoted scalar + $output = self::parseQuotedScalar($scalar, $i); + } + else + { + // "normal" string + if (!$delimiters) + { + $output = substr($scalar, $i); + $i += strlen($output); + + // remove comments + if (false !== $strpos = strpos($output, ' #')) + { + $output = rtrim(substr($output, 0, $strpos)); + } + } + else if (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) + { + $output = $match[1]; + $i += strlen($output); + } + else + { + throw new ParserException(sprintf('Malformed inline YAML string (%s).', $scalar)); + } + + $output = $evaluate ? self::evaluateScalar($output) : $output; + } + + return $output; + } + + /** + * Parses a quoted scalar to YAML. + * + * @param string $scalar + * @param integer $i + * + * @return string A YAML string + */ + static protected function parseQuotedScalar($scalar, &$i) + { + if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/A', substr($scalar, $i), $match)) + { + throw new ParserException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); + } + + $output = substr($match[0], 1, strlen($match[0]) - 2); + + if ('"' == $scalar[$i]) + { + // evaluate the string + $output = str_replace(array('\\"', '\\n', '\\r'), array('"', "\n", "\r"), $output); + } + else + { + // unescape ' + $output = str_replace('\'\'', '\'', $output); + } + + $i += strlen($match[0]); + + return $output; + } + + /** + * Parses a sequence to a YAML string. + * + * @param string $sequence + * @param integer $i + * + * @return string A YAML string + */ + static protected function parseSequence($sequence, &$i = 0) + { + $output = array(); + $len = strlen($sequence); + $i += 1; + + // [foo, bar, ...] + while ($i < $len) + { + switch ($sequence[$i]) + { + case '[': + // nested sequence + $output[] = self::parseSequence($sequence, $i); + break; + case '{': + // nested mapping + $output[] = self::parseMapping($sequence, $i); + break; + case ']': + return $output; + case ',': + case ' ': + break; + default: + $isQuoted = in_array($sequence[$i], array('"', "'")); + $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i); + + if (!$isQuoted && false !== strpos($value, ': ')) + { + // embedded mapping? + try + { + $value = self::parseMapping('{'.$value.'}'); + } + catch (\InvalidArgumentException $e) + { + // no, it's not + } + } + + $output[] = $value; + + --$i; + } + + ++$i; + } + + throw new ParserException(sprintf('Malformed inline YAML string %s', $sequence)); + } + + /** + * Parses a mapping to a YAML string. + * + * @param string $mapping + * @param integer $i + * + * @return string A YAML string + */ + static protected function parseMapping($mapping, &$i = 0) + { + $output = array(); + $len = strlen($mapping); + $i += 1; + + // {foo: bar, bar:foo, ...} + while ($i < $len) + { + switch ($mapping[$i]) + { + case ' ': + case ',': + ++$i; + continue 2; + case '}': + return $output; + } + + // key + $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); + + // value + $done = false; + while ($i < $len) + { + switch ($mapping[$i]) + { + case '[': + // nested sequence + $output[$key] = self::parseSequence($mapping, $i); + $done = true; + break; + case '{': + // nested mapping + $output[$key] = self::parseMapping($mapping, $i); + $done = true; + break; + case ':': + case ' ': + break; + default: + $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + $done = true; + --$i; + } + + ++$i; + + if ($done) + { + continue 2; + } + } + } + + throw new ParserException(sprintf('Malformed inline YAML string %s', $mapping)); + } + + /** + * Evaluates scalars and replaces magic values. + * + * @param string $scalar + * + * @return string A YAML string + */ + static protected function evaluateScalar($scalar) + { + $scalar = trim($scalar); + + $trueValues = '1.1' == Yaml::getSpecVersion() ? array('true', 'on', '+', 'yes', 'y') : array('true'); + $falseValues = '1.1' == Yaml::getSpecVersion() ? array('false', 'off', '-', 'no', 'n') : array('false'); + + switch (true) + { + case 'null' == strtolower($scalar): + case '' == $scalar: + case '~' == $scalar: + return null; + case 0 === strpos($scalar, '!str'): + return (string) substr($scalar, 5); + case 0 === strpos($scalar, '! '): + return intval(self::parseScalar(substr($scalar, 2))); + case 0 === strpos($scalar, '!!php/object:'): + return unserialize(substr($scalar, 13)); + case ctype_digit($scalar): + $raw = $scalar; + $cast = intval($scalar); + return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); + case in_array(strtolower($scalar), $trueValues): + return true; + case in_array(strtolower($scalar), $falseValues): + return false; + case is_numeric($scalar): + return '0x' == $scalar[0].$scalar[1] ? hexdec($scalar) : floatval($scalar); + case 0 == strcasecmp($scalar, '.inf'): + case 0 == strcasecmp($scalar, '.NaN'): + return -log(0); + case 0 == strcasecmp($scalar, '-.inf'): + return log(0); + case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): + return floatval(str_replace(',', '', $scalar)); + case preg_match(self::getTimestampRegex(), $scalar): + return strtotime($scalar); + default: + return (string) $scalar; + } + } + + static protected function getTimestampRegex() + { + return <<[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)? + $~x +EOF; + } +} diff --git a/Doctrine/Symfony/Component/Yaml/Parser.php b/Doctrine/Symfony/Component/Yaml/Parser.php new file mode 100644 index 0000000..c8c39ed --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/Parser.php @@ -0,0 +1,587 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Parser parses YAML strings to convert them to PHP arrays. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class Parser +{ + protected $offset = 0; + protected $lines = array(); + protected $currentLineNb = -1; + protected $currentLine = ''; + protected $refs = array(); + + /** + * Constructor + * + * @param integer $offset The offset of YAML document (used for line numbers in error messages) + */ + public function __construct($offset = 0) + { + $this->offset = $offset; + } + + /** + * Parses a YAML string to a PHP value. + * + * @param string $value A YAML string + * + * @return mixed A PHP value + * + * @throws \InvalidArgumentException If the YAML is not valid + */ + public function parse($value) + { + $this->currentLineNb = -1; + $this->currentLine = ''; + $this->lines = explode("\n", $this->cleanup($value)); + + $data = array(); + while ($this->moveToNextLine()) + { + if ($this->isCurrentLineEmpty()) + { + continue; + } + + // tab? + if (preg_match('#^\t+#', $this->currentLine)) + { + throw new ParserException(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + + $isRef = $isInPlace = $isProcessed = false; + if (preg_match('#^\-((?P\s+)(?P.+?))?\s*$#', $this->currentLine, $values)) + { + if (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#', $values['value'], $matches)) + { + $isRef = $matches['ref']; + $values['value'] = $matches['value']; + } + + // array + if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) + { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $data[] = $parser->parse($this->getNextEmbedBlock()); + } + else + { + if (isset($values['leadspaces']) + && ' ' == $values['leadspaces'] + && preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{].*?) *\:(\s+(?P.+?))?\s*$#', $values['value'], $matches)) + { + // this is a compact notation element, add to next block and parse + $c = $this->getRealCurrentLineNb(); + $parser = new Parser($c); + $parser->refs =& $this->refs; + + $block = $values['value']; + if (!$this->isNextLineIndented()) + { + $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + 2); + } + + $data[] = $parser->parse($block); + } + else + { + $data[] = $this->parseValue($values['value']); + } + } + } + else if (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"].*?) *\:(\s+(?P.+?))?\s*$#', $this->currentLine, $values)) + { + $key = Inline::parseScalar($values['key']); + + if ('<<' === $key) + { + if (isset($values['value']) && '*' === substr($values['value'], 0, 1)) + { + $isInPlace = substr($values['value'], 1); + if (!array_key_exists($isInPlace, $this->refs)) + { + throw new ParserException(sprintf('Reference "%s" does not exist at line %s (%s).', $isInPlace, $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + } + else + { + if (isset($values['value']) && $values['value'] !== '') + { + $value = $values['value']; + } + else + { + $value = $this->getNextEmbedBlock(); + } + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $parsed = $parser->parse($value); + + $merged = array(); + if (!is_array($parsed)) + { + throw new ParserException(sprintf("YAML merge keys used with a scalar value instead of an array at line %s (%s)", $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + else if (isset($parsed[0])) + { + // Numeric array, merge individual elements + foreach (array_reverse($parsed) as $parsedItem) + { + if (!is_array($parsedItem)) + { + throw new ParserException(sprintf("Merge items must be arrays at line %s (%s).", $this->getRealCurrentLineNb() + 1, $parsedItem)); + } + $merged = array_merge($parsedItem, $merged); + } + } + else + { + // Associative array, merge + $merged = array_merge($merge, $parsed); + } + + $isProcessed = $merged; + } + } + else if (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#', $values['value'], $matches)) + { + $isRef = $matches['ref']; + $values['value'] = $matches['value']; + } + + if ($isProcessed) + { + // Merge keys + $data = $isProcessed; + } + // hash + else if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) + { + // if next line is less indented or equal, then it means that the current value is null + if ($this->isNextLineIndented()) + { + $data[$key] = null; + } + else + { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $data[$key] = $parser->parse($this->getNextEmbedBlock()); + } + } + else + { + if ($isInPlace) + { + $data = $this->refs[$isInPlace]; + } + else + { + $data[$key] = $this->parseValue($values['value']); + } + } + } + else + { + // 1-liner followed by newline + if (2 == count($this->lines) && empty($this->lines[1])) + { + $value = Inline::load($this->lines[0]); + if (is_array($value)) + { + $first = reset($value); + if ('*' === substr($first, 0, 1)) + { + $data = array(); + foreach ($value as $alias) + { + $data[] = $this->refs[substr($alias, 1)]; + } + $value = $data; + } + } + + return $value; + } + + switch (preg_last_error()) + { + case PREG_INTERNAL_ERROR: + $error = 'Internal PCRE error on line'; + break; + case PREG_BACKTRACK_LIMIT_ERROR: + $error = 'pcre.backtrack_limit reached on line'; + break; + case PREG_RECURSION_LIMIT_ERROR: + $error = 'pcre.recursion_limit reached on line'; + break; + case PREG_BAD_UTF8_ERROR: + $error = 'Malformed UTF-8 data on line'; + break; + case PREG_BAD_UTF8_OFFSET_ERROR: + $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point on line'; + break; + default: + $error = 'Unable to parse line'; + } + + throw new ParserException(sprintf('%s %d (%s).', $error, $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + + if ($isRef) + { + $this->refs[$isRef] = end($data); + } + } + + return empty($data) ? null : $data; + } + + /** + * Returns the current line number (takes the offset into account). + * + * @return integer The current line number + */ + protected function getRealCurrentLineNb() + { + return $this->currentLineNb + $this->offset; + } + + /** + * Returns the current line indentation. + * + * @return integer The current line indentation + */ + protected function getCurrentLineIndentation() + { + return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' ')); + } + + /** + * Returns the next embed block of YAML. + * + * @param integer $indentation The indent level at which the block is to be read, or null for default + * + * @return string A YAML string + */ + protected function getNextEmbedBlock($indentation = null) + { + $this->moveToNextLine(); + + if (null === $indentation) + { + $newIndent = $this->getCurrentLineIndentation(); + + if (!$this->isCurrentLineEmpty() && 0 == $newIndent) + { + throw new ParserException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + } + else + { + $newIndent = $indentation; + } + + $data = array(substr($this->currentLine, $newIndent)); + + while ($this->moveToNextLine()) + { + if ($this->isCurrentLineEmpty()) + { + if ($this->isCurrentLineBlank()) + { + $data[] = substr($this->currentLine, $newIndent); + } + + continue; + } + + $indent = $this->getCurrentLineIndentation(); + + if (preg_match('#^(?P *)$#', $this->currentLine, $match)) + { + // empty line + $data[] = $match['text']; + } + else if ($indent >= $newIndent) + { + $data[] = substr($this->currentLine, $newIndent); + } + else if (0 == $indent) + { + $this->moveToPreviousLine(); + + break; + } + else + { + throw new ParserException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + } + } + + return implode("\n", $data); + } + + /** + * Moves the parser to the next line. + */ + protected function moveToNextLine() + { + if ($this->currentLineNb >= count($this->lines) - 1) + { + return false; + } + + $this->currentLine = $this->lines[++$this->currentLineNb]; + + return true; + } + + /** + * Moves the parser to the previous line. + */ + protected function moveToPreviousLine() + { + $this->currentLine = $this->lines[--$this->currentLineNb]; + } + + /** + * Parses a YAML value. + * + * @param string $value A YAML value + * + * @return mixed A PHP value + */ + protected function parseValue($value) + { + if ('*' === substr($value, 0, 1)) + { + if (false !== $pos = strpos($value, '#')) + { + $value = substr($value, 1, $pos - 2); + } + else + { + $value = substr($value, 1); + } + + if (!array_key_exists($value, $this->refs)) + { + throw new ParserException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine)); + } + return $this->refs[$value]; + } + + if (preg_match('/^(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?$/', $value, $matches)) + { + $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; + + return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers))); + } + else + { + return Inline::load($value); + } + } + + /** + * Parses a folded scalar. + * + * @param string $separator The separator that was used to begin this folded scalar (| or >) + * @param string $indicator The indicator that was used to begin this folded scalar (+ or -) + * @param integer $indentation The indentation that was used to begin this folded scalar + * + * @return string The text value + */ + protected function parseFoldedScalar($separator, $indicator = '', $indentation = 0) + { + $separator = '|' == $separator ? "\n" : ' '; + $text = ''; + + $notEOF = $this->moveToNextLine(); + + while ($notEOF && $this->isCurrentLineBlank()) + { + $text .= "\n"; + + $notEOF = $this->moveToNextLine(); + } + + if (!$notEOF) + { + return ''; + } + + if (!preg_match('#^(?P'.($indentation ? str_repeat(' ', $indentation) : ' +').')(?P.*)$#', $this->currentLine, $matches)) + { + $this->moveToPreviousLine(); + + return ''; + } + + $textIndent = $matches['indent']; + $previousIndent = 0; + + $text .= $matches['text'].$separator; + while ($this->currentLineNb + 1 < count($this->lines)) + { + $this->moveToNextLine(); + + if (preg_match('#^(?P {'.strlen($textIndent).',})(?P.+)$#', $this->currentLine, $matches)) + { + if (' ' == $separator && $previousIndent != $matches['indent']) + { + $text = substr($text, 0, -1)."\n"; + } + $previousIndent = $matches['indent']; + + $text .= str_repeat(' ', $diff = strlen($matches['indent']) - strlen($textIndent)).$matches['text'].($diff ? "\n" : $separator); + } + else if (preg_match('#^(?P *)$#', $this->currentLine, $matches)) + { + $text .= preg_replace('#^ {1,'.strlen($textIndent).'}#', '', $matches['text'])."\n"; + } + else + { + $this->moveToPreviousLine(); + + break; + } + } + + if (' ' == $separator) + { + // replace last separator by a newline + $text = preg_replace('/ (\n*)$/', "\n$1", $text); + } + + switch ($indicator) + { + case '': + $text = preg_replace('#\n+$#s', "\n", $text); + break; + case '+': + break; + case '-': + $text = preg_replace('#\n+$#s', '', $text); + break; + } + + return $text; + } + + /** + * Returns true if the next line is indented. + * + * @return Boolean Returns true if the next line is indented, false otherwise + */ + protected function isNextLineIndented() + { + $currentIndentation = $this->getCurrentLineIndentation(); + $notEOF = $this->moveToNextLine(); + + while ($notEOF && $this->isCurrentLineEmpty()) + { + $notEOF = $this->moveToNextLine(); + } + + if (false === $notEOF) + { + return false; + } + + $ret = false; + if ($this->getCurrentLineIndentation() <= $currentIndentation) + { + $ret = true; + } + + $this->moveToPreviousLine(); + + return $ret; + } + + /** + * Returns true if the current line is blank or if it is a comment line. + * + * @return Boolean Returns true if the current line is empty or if it is a comment line, false otherwise + */ + protected function isCurrentLineEmpty() + { + return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); + } + + /** + * Returns true if the current line is blank. + * + * @return Boolean Returns true if the current line is blank, false otherwise + */ + protected function isCurrentLineBlank() + { + return '' == trim($this->currentLine, ' '); + } + + /** + * Returns true if the current line is a comment line. + * + * @return Boolean Returns true if the current line is a comment line, false otherwise + */ + protected function isCurrentLineComment() + { + //checking explicitly the first char of the trim is faster than loops or strpos + $ltrimmedLine = ltrim($this->currentLine, ' '); + return $ltrimmedLine[0] === '#'; + } + + /** + * Cleanups a YAML string to be parsed. + * + * @param string $value The input YAML string + * + * @return string A cleaned up YAML string + */ + protected function cleanup($value) + { + $value = str_replace(array("\r\n", "\r"), "\n", $value); + + if (!preg_match("#\n$#", $value)) + { + $value .= "\n"; + } + + // strip YAML header + $count = 0; + $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#s', '', $value, -1, $count); + $this->offset += $count; + + // remove leading comments and/or --- + $trimmedValue = preg_replace('#^((\#.*?\n)|(\-\-\-.*?\n))*#s', '', $value, -1, $count); + if ($count == 1) + { + // items have been removed, update the offset + $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); + $value = $trimmedValue; + } + + return $value; + } +} diff --git a/Doctrine/Symfony/Component/Yaml/ParserException.php b/Doctrine/Symfony/Component/Yaml/ParserException.php new file mode 100644 index 0000000..5683d8c --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/ParserException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Exception class used by all exceptions thrown by the component. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class ParserException extends Exception +{ +} diff --git a/Doctrine/Symfony/Component/Yaml/Yaml.php b/Doctrine/Symfony/Component/Yaml/Yaml.php new file mode 100644 index 0000000..7bdc180 --- /dev/null +++ b/Doctrine/Symfony/Component/Yaml/Yaml.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Yaml offers convenience methods to load and dump YAML. + * + * @package symfony + * @subpackage yaml + * @author Fabien Potencier + */ +class Yaml +{ + static protected $spec = '1.2'; + + /** + * Sets the YAML specification version to use. + * + * @param string $version The YAML specification version + */ + static public function setSpecVersion($version) + { + if (!in_array($version, array('1.1', '1.2'))) + { + throw new \InvalidArgumentException(sprintf('Version %s of the YAML specifications is not supported', $version)); + } + + self::$spec = $version; + } + + /** + * Gets the YAML specification version to use. + * + * @return string The YAML specification version + */ + static public function getSpecVersion() + { + return self::$spec; + } + + /** + * Loads YAML into a PHP array. + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. + * + * Usage: + * + * $array = Yaml::load('config.yml'); + * print_r($array); + * + * + * @param string $input Path of YAML file or string containing YAML + * + * @return array The YAML converted to a PHP array + * + * @throws \InvalidArgumentException If the YAML is not valid + */ + public static function load($input) + { + $file = ''; + + // if input is a file, process it + if (strpos($input, "\n") === false && is_file($input)) + { + $file = $input; + + ob_start(); + $retval = include($input); + $content = ob_get_clean(); + + // if an array is returned by the config file assume it's in plain php form else in YAML + $input = is_array($retval) ? $retval : $content; + } + + // if an array is returned by the config file assume it's in plain php form else in YAML + if (is_array($input)) + { + return $input; + } + + $yaml = new Parser(); + + try + { + $ret = $yaml->parse($input); + } + catch (\Exception $e) + { + throw new \InvalidArgumentException(sprintf('Unable to parse %s: %s', $file ? sprintf('file "%s"', $file) : 'string', $e->getMessage())); + } + + return $ret; + } + + /** + * Dumps a PHP array to a YAML string. + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. + * + * @param array $array PHP array + * @param integer $inline The level where you switch to inline YAML + * + * @return string A YAML string representing the original PHP array + */ + public static function dump($array, $inline = 2) + { + $yaml = new Dumper(); + + return $yaml->dump($array, $inline); + } +} diff --git a/Proxies/ScoreProxy.php b/Proxies/ScoreProxy.php new file mode 100644 index 0000000..16ca4fd --- /dev/null +++ b/Proxies/ScoreProxy.php @@ -0,0 +1,35 @@ +_entityPersister = $entityPersister; + $this->_identifier = $identifier; + } + private function _load() + { + if (!$this->__isInitialized__ && $this->_entityPersister) { + $this->__isInitialized__ = true; + if ($this->_entityPersister->load($this->_identifier, $this) === null) { + throw new \Doctrine\ORM\EntityNotFoundException(); + } + unset($this->_entityPersister, $this->_identifier); + } + } + + + + public function __sleep() + { + return array('__isInitialized__', 'gscore', 'id', 'user_id'); + } +} \ No newline at end of file diff --git a/Proxies/SessionProxy.php b/Proxies/SessionProxy.php new file mode 100644 index 0000000..29fadb5 --- /dev/null +++ b/Proxies/SessionProxy.php @@ -0,0 +1,35 @@ +_entityPersister = $entityPersister; + $this->_identifier = $identifier; + } + private function _load() + { + if (!$this->__isInitialized__ && $this->_entityPersister) { + $this->__isInitialized__ = true; + if ($this->_entityPersister->load($this->_identifier, $this) === null) { + throw new \Doctrine\ORM\EntityNotFoundException(); + } + unset($this->_entityPersister, $this->_identifier); + } + } + + + + public function __sleep() + { + return array('__isInitialized__', 'sessionkey', 'stime', 'id', 'user_id'); + } +} \ No newline at end of file diff --git a/Proxies/UserProxy.php b/Proxies/UserProxy.php new file mode 100644 index 0000000..91abb66 --- /dev/null +++ b/Proxies/UserProxy.php @@ -0,0 +1,89 @@ +_entityPersister = $entityPersister; + $this->_identifier = $identifier; + } + private function _load() + { + if (!$this->__isInitialized__ && $this->_entityPersister) { + $this->__isInitialized__ = true; + if ($this->_entityPersister->load($this->_identifier, $this) === null) { + throw new \Doctrine\ORM\EntityNotFoundException(); + } + unset($this->_entityPersister, $this->_identifier); + } + } + + + public function id() + { + $this->_load(); + return parent::id(); + } + + public function getName() + { + $this->_load(); + return parent::getName(); + } + + public function setName($name) + { + $this->_load(); + return parent::setName($name); + } + + public function getPass() + { + $this->_load(); + return parent::getPass(); + } + + public function setPass($pass) + { + $this->_load(); + return parent::setPass($pass); + } + + public function addScore($score) + { + $this->_load(); + return parent::addScore($score); + } + + public function getScores() + { + $this->_load(); + return parent::getScores(); + } + + public function addSession($session) + { + $this->_load(); + return parent::addSession($session); + } + + public function getSessions() + { + $this->_load(); + return parent::getSessions(); + } + + + public function __sleep() + { + return array('__isInitialized__', 'name', 'pass', 'id', 'scores', 'sessions'); + } +} \ No newline at end of file diff --git a/Tim/Score.php b/Tim/Score.php new file mode 100644 index 0000000..9171b2f --- /dev/null +++ b/Tim/Score.php @@ -0,0 +1,33 @@ +id; + } + + function getGscore() + { + return $this->gscore; + } + + function setGscore($gscore) + { + $this->gscore = $gscore; + } + + function getUser() + { + return $this->user; + } + + function setUser($user) + { + $this->user = $user; + } +} +?> \ No newline at end of file diff --git a/Tim/Session.php b/Tim/Session.php new file mode 100644 index 0000000..8fd42b2 --- /dev/null +++ b/Tim/Session.php @@ -0,0 +1,44 @@ +id; + } + + function getSessionkey() + { + return $this->sessionkey; + } + + function setSessionkey($sessionkey) + { + $this->sessionkey = $sessionkey; + } + + function getStime() + { + return $this->stime; + } + + function setStime($stime) + { + $this->stime = $stime; + } + + function getUser() + { + return $this->user; + } + + function setUser($user) + { + $this->user = $user; + } +} +?> \ No newline at end of file diff --git a/Tim/User.php b/Tim/User.php new file mode 100644 index 0000000..134c537 --- /dev/null +++ b/Tim/User.php @@ -0,0 +1,64 @@ +scores = new ArrayCollection(); + $this->sessions = new ArrayCollection(); + } + + function id() + { + return $this->id; + } + + function getName() + { + return $this->name; + } + + function setName($name) + { + $this->name = $name; + } + + function getPass() + { + return $this->pass; + } + + function setPass($pass) + { + $this->pass = $pass; + } + + public function addScore($score) + { + $this->scores[] =$score; + } + + public function getScores() + { + return $this->scores; + } + + public function addSession($session) + { + $this->sessions[] = $session; + } + + public function getSessions() + { + return $this->sessions; + } +} +?> \ No newline at end of file diff --git a/chatserver.php b/chatserver.php new file mode 100644 index 0000000..5999c4f --- /dev/null +++ b/chatserver.php @@ -0,0 +1,146 @@ +createQuery($dql) + ->setParameter(1, $sessionkey) + ->setMaxResults(1) + ->getResult(); + + if (!$result) + return false; + + return $result[0]->getUser()->getName(); +} + +// create a streaming socket, of type TCP/IP +$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + +// set the option to reuse the port +socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1); + +// "bind" the socket to the address to "localhost", on port $port +// so this means that all connections on this port are now our resposibility to send/recv data, disconnect, etc.. +socket_bind($sock, 0, $port); + +// start listen for connections +socket_listen($sock); + +// create a list of all the clients that will be connected to us.. +// add the listening socket to this list +$clients = array($sock); + +function genkey() +{ + global $clients; + static $counter = 0; + + do { + $key = md5(++$counter . uniqid()); + } while (array_key_exists($key, $clients)); + return $key; +} + + +$client_names = array(); + +while (true) { + // create a copy, so $clients doesn't get modified by socket_select() + $read = $clients; + + // get a list of all the clients that have data to be read from + // if there are no clients with data, go to next iteration + if (socket_select($read, $write = NULL, $except = NULL, 0) < 1) + continue; + + // check if there is a client trying to connect + if (in_array($sock, $read)) { + // accept the client, and add him to the $clients array + $newkey = genkey(); + $clients[$newkey] = $newsock = socket_accept($sock); + $client_names[$newkey] = ''; + + // send the client a welcome message + socket_write($newsock, ">> Connected. There are ".(count($clients) - 1)." client(s) connected to the server\n>> Type /quit to closse chat view.\n"); + + socket_getpeername($newsock, $ip); + echo "New client connected: {$ip}\n"; + + // remove the listening socket from the clients-with-data array + $key = array_search($sock, $read); + unset($read[$key]); + } + + // loop through all the clients that have data to read from + foreach ($read as $read_sock) { + // read until newline or 1024 bytes + // socket_read while show errors when the client is disconnected, so silence the error messages + $data = @socket_read($read_sock, 1024, PHP_NORMAL_READ); + + $key = array_search($read_sock, $clients); + + // check if the client is disconnected + if ($data === false) { + // remove client for $clients array + unset($clients[$key]); + unset($client_names[$key]); + echo "client disconnected.\n"; + // continue to the next client to read from, if any + continue; + } + + // trim off the trailing/beginning white spaces + $data = trim($data); + + // check if there is any data after trimming off the spaces + if (!empty($data)) { + + if (empty($client_names[$key])) + { + $result = auth($data); + if ($result !== false) + { + $client_names[$key] = $result; + continue; + } + socket_write($read_sock, ">> AUTH ERROR\n"); + continue; + } + + $sender_name = $client_names[$key]; + // send this to all the clients in the $clients array (except the first one, which is a listening socket) + foreach ($clients as $send_sock) { + + // if its the listening sock or the client that we got the message from, go to the next one in the list + if ($send_sock == $sock || $send_sock == $read_sock) + continue; + + // write the message to the client -- add a newline character to the end of the message + socket_write($send_sock, $sender_name . ": " . $data."\n"); + + } // end of broadcast foreach + + } + + } // end of reading foreach +} + +// close the listening socket +socket_close($sock); \ No newline at end of file diff --git a/config/mappings/Score.dcm.xml b/config/mappings/Score.dcm.xml new file mode 100644 index 0000000..f945edb --- /dev/null +++ b/config/mappings/Score.dcm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/mappings/Session.dcm.xml b/config/mappings/Session.dcm.xml new file mode 100644 index 0000000..9bd6f01 --- /dev/null +++ b/config/mappings/Session.dcm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/config/mappings/User.dcm.xml b/config/mappings/User.dcm.xml new file mode 100644 index 0000000..47b3202 --- /dev/null +++ b/config/mappings/User.dcm.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doctrine-orm/LICENSE b/doctrine-orm/LICENSE new file mode 100644 index 0000000..1c03f74 --- /dev/null +++ b/doctrine-orm/LICENSE @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/doctrine-orm/bin/doctrine b/doctrine-orm/bin/doctrine new file mode 100644 index 0000000..92f323f --- /dev/null +++ b/doctrine-orm/bin/doctrine @@ -0,0 +1,4 @@ +#!/usr/bin/env php +. + */ + +require_once 'Doctrine/Common/ClassLoader.php'; + +$classLoader = new \Doctrine\Common\ClassLoader('Doctrine'); +$classLoader->register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Symfony', 'Doctrine'); +$classLoader->register(); + +$configFile = getcwd() . DIRECTORY_SEPARATOR . 'cli-config.php'; + +$helperSet = null; +if (file_exists($configFile)) { + if ( ! is_readable($configFile)) { + trigger_error( + 'Configuration file [' . $configFile . '] does not have read permission.', E_ERROR + ); + } + + require $configFile; + + foreach ($GLOBALS as $helperSetCandidate) { + if ($helperSetCandidate instanceof \Symfony\Component\Console\Helper\HelperSet) { + $helperSet = $helperSetCandidate; + break; + } + } +} + +$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet(); + +\Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet); diff --git a/doctrine.php b/doctrine.php new file mode 100644 index 0000000..cbe7443 --- /dev/null +++ b/doctrine.php @@ -0,0 +1,10 @@ + new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($entityManager) +)); + +$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet(); + +\Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet); diff --git a/index.php b/index.php new file mode 100644 index 0000000..d0e29e8 --- /dev/null +++ b/index.php @@ -0,0 +1,148 @@ +createQuery($dql) + ->setParameter(1, $_GET['user']) + ->setMaxResults(1) + ->getResult(); + + if (!$result) + return error('No such user'); + + $user = $result[0]; + + if ($user->getPass() != $_GET['pass']) + return error('Wrong password'); + + $session = new Session(); + $session->setSessionkey(md5(time() . uniqid())); + $session->setStime(new DateTime("now")); + $session->setUser($user); + + $entityManager->persist($session); + $entityManager->flush(); + return success("\n\t\t" . $session->getSessionkey() . "\n\t\tgetName() . "]]>\n\t\t" . $user->id() . "\n\t"); +} + +function register() { + global $entityManager; + + if (!isset($_GET['user']) || !isset($_GET['pass'])) + return error('No username or password provided'); + + $dql = "SELECT u FROM User u WHERE u.name=?1"; + + $result = $entityManager->createQuery($dql) + ->setParameter(1, $_GET['user']) + ->setMaxResults(1) + ->getResult(); + + if ($result) + return error('User already exists'); + + $user = new User; + $user->setName($_GET['user']); + $user->setPass($_GET['pass']); + + $entityManager->persist($user); + $entityManager->flush(); + return success(''); +} + +function error($msg) +{ + print ' + + + +'; +} + +function success($message) +{ + print ' + + ' . $message . ' +'; +} + +function score() +{ + global $entityManager; + + $dql = "SELECT s, u FROM Score s JOIN s.user u ORDER BY s.gscore DESC"; + + $myScores = $entityManager->createQuery($dql) + ->setMaxResults(20) + ->getResult(); + + print "\n\n\t\n"; + + $position = 0; + foreach ($myScores AS $score) + { + print "\t\t\n" + . "\t\t\t" . ++$position . "\n" + . "\t\t\t" . $score->getUser()->id() . "\n" + . "\t\t\t" . $score->getUser()->getName() . "\n" + . "\t\t\t" . $score->getGscore() . "\n" + . "\t\t\n"; + } + print "\t\n\n"; + +} + +function set_score() +{ + global $entityManager; + + if (!isset($_GET['s'])) + return error('Log in first'); + if (!isset($_GET['score'])) + return error('No score provided'); + + $dql = "SELECT s FROM Session s WHERE s.sessionkey=?1"; + + $result = $entityManager->createQuery($dql) + ->setParameter(1, $_GET['s']) + ->setMaxResults(1) + ->getResult(); + + if (!$result) + return error('Log in first'); + + $session = $result[0]; + + + $score = new Score(); + $score->setGscore((int) $_GET['score']); + $score->setUser($session->getUser()); + + $entityManager->persist($score); + $entityManager->flush(); + return success(''); +} diff --git a/init.php b/init.php new file mode 100644 index 0000000..0dc360d --- /dev/null +++ b/init.php @@ -0,0 +1,45 @@ + 'tim', + 'user' => 'tim', + 'password' => 'tim', + 'host' => 'localhost', + 'driver' => 'pdo_pgsql', +); + +$classLoader = new \Doctrine\Common\ClassLoader('Doctrine'); +$classLoader->register(); + +$classLoader = new \Doctrine\Common\ClassLoader('Symfony', 'Doctrine'); +$classLoader->register(); + +$classLoader = new \Doctrine\Common\ClassLoader(null, 'Tim'); +$classLoader->register(); + +$config = new Doctrine\ORM\Configuration(); + +// Mapping Configuration +$driverImpl = new Doctrine\ORM\Mapping\Driver\XmlDriver('config/mappings/'); +$config->setMetadataDriverImpl($driverImpl); + +$config->setProxyDir(__DIR__.'/Proxies'); +$config->setProxyNamespace('Proxies'); +$config->setAutoGenerateProxyClasses((APPLICATION_ENV == "development")); + +// Caching Configuration +if (APPLICATION_ENV == "development") { + $cache = new \Doctrine\Common\Cache\ArrayCache(); +} else { + $cache = new \Doctrine\Common\Cache\ApcCache(); +} +$config->setMetadataCacheImpl($cache); +$config->setQueryCacheImpl($cache); + + +// Obtaining the entity manager +$evm = new Doctrine\Common\EventManager(); +$entityManager = \Doctrine\ORM\EntityManager::create($conn, $config, $evm); -- 2.52.0